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/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; } 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..cad19d8 --- /dev/null +++ b/SeeShark/Device/WindowManager.cs @@ -0,0 +1,154 @@ +// 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(); + + 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) + { + 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 = $"0x{new IntPtr(0x3600003).ToString("X2")}" + }; + } +} diff --git a/SeeShark/Interop/Windows/User32.cs b/SeeShark/Interop/Windows/User32.cs index d994206..5946a3b 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,21 @@ 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 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); } 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..f64a542 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. /// @@ -107,6 +112,12 @@ public class VideoInputOptions } } + if (WindowId != null) + { + if (deviceFormat == DeviceInputFormat.X11Grab) + dict.Add("window_id", WindowId); + } + switch (deviceFormat) { case DeviceInputFormat.X11Grab: