diff --git a/bindings/csharp/src/CNativeAPI/generated/CaptureSource.cs b/bindings/csharp/src/CNativeAPI/generated/CaptureSource.cs new file mode 100644 index 00000000..519ac7fa --- /dev/null +++ b/bindings/csharp/src/CNativeAPI/generated/CaptureSource.cs @@ -0,0 +1,28 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace CNativeAPI; + +public static partial class Interop +{ + [DllImport(Libraries.NativeApi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr native_capture_source_get_name(ulong self); + + [DllImport(Libraries.NativeApi, CallingConvention = CallingConvention.Cdecl)] + public static extern int native_capture_source_get_type(ulong self); + + [DllImport(Libraries.NativeApi, CallingConvention = CallingConvention.Cdecl)] + public static extern ulong native_capture_source_from_display(ulong display); + + [DllImport(Libraries.NativeApi, CallingConvention = CallingConvention.Cdecl)] + public static extern ulong native_capture_source_from_window(ulong window); + + [DllImport(Libraries.NativeApi, CallingConvention = CallingConvention.Cdecl)] + public static extern void native_capture_source_free(ulong handle); +} + diff --git a/bindings/csharp/src/CNativeAPI/generated/ScreenCapturer.cs b/bindings/csharp/src/CNativeAPI/generated/ScreenCapturer.cs new file mode 100644 index 00000000..c8abdfe6 --- /dev/null +++ b/bindings/csharp/src/CNativeAPI/generated/ScreenCapturer.cs @@ -0,0 +1,59 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace CNativeAPI; + +[StructLayout(LayoutKind.Sequential)] +public struct native_capture_source_selection_options_t +{ + public byte allow_displays; + public byte allow_windows; + public ulong parent_window; +} + +[StructLayout(LayoutKind.Sequential)] +public struct native_screen_capture_options_t +{ + public byte include_cursor; + public byte crop_enabled; + public native_rectangle_t crop; +} + +[UnmanagedFunctionPointer(CallingConvention.Cdecl)] +public delegate void ScreenCapturerCaptureCallbackNativeCallback([MarshalAs(UnmanagedType.I1)] bool arg0, ulong arg1, ulong delivery, IntPtr userData); + +[UnmanagedFunctionPointer(CallingConvention.Cdecl)] +public delegate void ScreenCapturerSelectSourceCallbackNativeCallback([MarshalAs(UnmanagedType.I1)] bool arg0, ulong arg1, ulong delivery, IntPtr userData); + +public static partial class Interop +{ + [DllImport(Libraries.NativeApi, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.I1)] + public static extern bool native_screen_capturer_is_direct_capture_supported(int type); + + [DllImport(Libraries.NativeApi, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.I1)] + public static extern bool native_screen_capturer_is_source_selection_supported(); + + [DllImport(Libraries.NativeApi, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.I1)] + public static extern bool native_screen_capturer_is_supported(); + + [DllImport(Libraries.NativeApi, CallingConvention = CallingConvention.Cdecl)] + public static extern void native_capture_source_selection_options_free(ref native_capture_source_selection_options_t value); + + [DllImport(Libraries.NativeApi, CallingConvention = CallingConvention.Cdecl)] + public static extern void native_screen_capture_options_free(ref native_screen_capture_options_t value); + + [DllImport(Libraries.NativeApi, CallingConvention = CallingConvention.Cdecl)] + public static extern void native_screen_capturer_capture(ulong source, native_screen_capture_options_t options, ScreenCapturerCaptureCallbackNativeCallback callback, IntPtr callback_user_data, ReleaseUserDataNativeCallback callback_release_user_data); + + [DllImport(Libraries.NativeApi, CallingConvention = CallingConvention.Cdecl)] + public static extern void native_screen_capturer_select_source(native_capture_source_selection_options_t options, ScreenCapturerSelectSourceCallbackNativeCallback callback, IntPtr callback_user_data, ReleaseUserDataNativeCallback callback_release_user_data); +} + diff --git a/bindings/csharp/src/CNativeAPI/generated/ScreenRecorder.cs b/bindings/csharp/src/CNativeAPI/generated/ScreenRecorder.cs new file mode 100644 index 00000000..bed4454a --- /dev/null +++ b/bindings/csharp/src/CNativeAPI/generated/ScreenRecorder.cs @@ -0,0 +1,96 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace CNativeAPI; + +[StructLayout(LayoutKind.Sequential)] +public struct native_screen_recording_options_t +{ + public native_screen_capture_options_t capture; + public double frame_rate; + public CULong video_bitrate; +} + +[StructLayout(LayoutKind.Sequential)] +public struct native_screen_recording_event_t +{ + public int type; + public DataUnion data; + + [StructLayout(LayoutKind.Explicit)] + public struct DataUnion + { + [FieldOffset(0)] public CompletedData completed; + [FieldOffset(0)] public FailedData failed; + } + + [StructLayout(LayoutKind.Sequential)] + public struct CompletedData + { + public IntPtr file_path; + public double duration; + } + + [StructLayout(LayoutKind.Sequential)] + public struct FailedData + { + public IntPtr message; + } +} + +[UnmanagedFunctionPointer(CallingConvention.Cdecl)] +public delegate void ScreenRecordingEventNativeCallback(IntPtr evt, IntPtr userData); + +public static partial class Interop +{ + [DllImport(Libraries.NativeApi, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.I1)] + public static extern bool native_screen_recorder_cancel(ulong self); + + [DllImport(Libraries.NativeApi, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.I1)] + public static extern bool native_screen_recorder_is_active(ulong self); + + [DllImport(Libraries.NativeApi, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.I1)] + public static extern bool native_screen_recorder_is_supported(); + + [DllImport(Libraries.NativeApi, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.I1)] + public static extern bool native_screen_recorder_remove_listener(ulong self, ulong listenerId); + + [DllImport(Libraries.NativeApi, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.I1)] + public static extern bool native_screen_recorder_start(ulong self, ulong source, [MarshalAs(UnmanagedType.LPUTF8Str)] string? filePath, native_screen_recording_options_t options); + + [DllImport(Libraries.NativeApi, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.I1)] + public static extern bool native_screen_recorder_stop(ulong self); + + [DllImport(Libraries.NativeApi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr native_screen_recorder_get_file_path(ulong self); + + [DllImport(Libraries.NativeApi, CallingConvention = CallingConvention.Cdecl)] + public static extern double native_screen_recorder_get_duration(ulong self); + + [DllImport(Libraries.NativeApi, CallingConvention = CallingConvention.Cdecl)] + public static extern int native_screen_recorder_get_state(ulong self); + + [DllImport(Libraries.NativeApi, CallingConvention = CallingConvention.Cdecl)] + public static extern ulong native_screen_recorder_add_listener(ulong self, ScreenRecordingEventNativeCallback callback, IntPtr userData, ReleaseUserDataNativeCallback releaseUserData); + + [DllImport(Libraries.NativeApi, CallingConvention = CallingConvention.Cdecl)] + public static extern ulong native_screen_recorder_create(); + + [DllImport(Libraries.NativeApi, CallingConvention = CallingConvention.Cdecl)] + public static extern void native_screen_recorder_free(ulong handle); + + [DllImport(Libraries.NativeApi, CallingConvention = CallingConvention.Cdecl)] + public static extern void native_screen_recording_options_free(ref native_screen_recording_options_t value); +} + diff --git a/bindings/csharp/src/NativeAPI/CaptureSource.cs b/bindings/csharp/src/NativeAPI/CaptureSource.cs new file mode 100644 index 00000000..aeafdbe8 --- /dev/null +++ b/bindings/csharp/src/NativeAPI/CaptureSource.cs @@ -0,0 +1,80 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using CNativeAPI; + +namespace NativeAPI; + +public enum CaptureSourceType +{ + None = 0, + Display = 1, + Window = 2, + Unknown = 3, +} + +/// Owned handle to a native CaptureSource. +public sealed partial class CaptureSource : IDisposable +{ + public ulong NativeHandle { get; private set; } + private readonly bool _ownsHandle; + + public CaptureSource(ulong nativeHandle, bool ownsHandle = true) + { + NativeHandle = nativeHandle; + _ownsHandle = ownsHandle; + } + + ~CaptureSource() => ReleaseHandle(); + + public void Dispose() + { + ReleaseHandle(); + GC.SuppressFinalize(this); + } + + private void ReleaseHandle() + { + if (_ownsHandle && NativeHandle != 0) + { + Interop.native_capture_source_free(NativeHandle); + NativeHandle = 0; + } + } + + public static CaptureSource? FromDisplay(Display? display) + { + var rawResult = Interop.native_capture_source_from_display(display?.NativeHandle ?? 0); + return rawResult == 0 ? null : new CaptureSource(rawResult); + } + + public static CaptureSource? FromWindow(Window? window) + { + var rawResult = Interop.native_capture_source_from_window(window?.NativeHandle ?? 0); + return rawResult == 0 ? null : new CaptureSource(rawResult); + } + + public CaptureSourceType Type + { + get + { + var rawResult = Interop.native_capture_source_get_type(NativeHandle); + return (CaptureSourceType)rawResult; + } + } + + public string? Name + { + get + { + var rawResult = Interop.native_capture_source_get_name(NativeHandle); + return Interop.ConsumeString(rawResult); + } + } + +} + diff --git a/bindings/csharp/src/NativeAPI/ScreenCapturer.cs b/bindings/csharp/src/NativeAPI/ScreenCapturer.cs new file mode 100644 index 00000000..f51ff888 --- /dev/null +++ b/bindings/csharp/src/NativeAPI/ScreenCapturer.cs @@ -0,0 +1,113 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using CNativeAPI; + +namespace NativeAPI; + +public struct CaptureSourceSelectionOptions +{ + public bool AllowDisplays; + public bool AllowWindows; + public Window? ParentWindow; + + public CaptureSourceSelectionOptions(bool allowDisplays, bool allowWindows, Window? parentWindow) + { + AllowDisplays = allowDisplays; + AllowWindows = allowWindows; + ParentWindow = parentWindow; + } + + internal static CaptureSourceSelectionOptions FromRaw(in native_capture_source_selection_options_t raw) + { + return new CaptureSourceSelectionOptions(raw.allow_displays != 0, raw.allow_windows != 0, raw.parent_window == 0 ? null : new Window(Interop.native_handle_retain(raw.parent_window))); + } + + internal native_capture_source_selection_options_t ToRaw() + { + var raw = new native_capture_source_selection_options_t(); + raw.allow_displays = (byte)(AllowDisplays ? 1 : 0); + raw.allow_windows = (byte)(AllowWindows ? 1 : 0); + raw.parent_window = ParentWindow?.NativeHandle ?? 0; + return raw; + } + + internal static void ReleaseRaw(ref native_capture_source_selection_options_t raw) + { + } +} + +public struct ScreenCaptureOptions +{ + public bool IncludeCursor; + public bool CropEnabled; + public Rectangle Crop; + + public ScreenCaptureOptions(bool includeCursor, bool cropEnabled, Rectangle crop) + { + IncludeCursor = includeCursor; + CropEnabled = cropEnabled; + Crop = crop; + } + + internal static ScreenCaptureOptions FromRaw(in native_screen_capture_options_t raw) + { + return new ScreenCaptureOptions(raw.include_cursor != 0, raw.crop_enabled != 0, Rectangle.FromRaw(in raw.crop)); + } + + internal native_screen_capture_options_t ToRaw() + { + var raw = new native_screen_capture_options_t(); + raw.include_cursor = (byte)(IncludeCursor ? 1 : 0); + raw.crop_enabled = (byte)(CropEnabled ? 1 : 0); + raw.crop = Crop.ToRaw(); + return raw; + } + + internal static void ReleaseRaw(ref native_screen_capture_options_t raw) + { + } +} + +public static partial class ScreenCapturer +{ + public static bool IsSupported() + { + var rawResult = Interop.native_screen_capturer_is_supported(); + return rawResult; + } + + public static bool IsSourceSelectionSupported() + { + var rawResult = Interop.native_screen_capturer_is_source_selection_supported(); + return rawResult; + } + + public static bool IsDirectCaptureSupported(CaptureSourceType type) + { + var rawResult = Interop.native_screen_capturer_is_direct_capture_supported((int)type); + return rawResult; + } + + public static void SelectSource(CaptureSourceSelectionOptions options, Action callback) + { + var rawOptions = options.ToRaw(); + ScreenCapturerSelectSourceCallbackNativeCallback nativeCallback = (arg0, arg1, delivery, userData) => { try { if (!Interop.native_event_delivery_is_active(delivery)) return; callback(arg0, arg1 == 0 ? null : new CaptureSource(Interop.native_handle_retain(arg1))); } catch (Exception) { } finally { Interop.native_event_delivery_complete(delivery, true); } }; + Interop.native_screen_capturer_select_source(rawOptions, nativeCallback, CallbackKeeper.Hold(nativeCallback), CallbackKeeper.Release); + CaptureSourceSelectionOptions.ReleaseRaw(ref rawOptions); + } + + public static void Capture(CaptureSource? source, ScreenCaptureOptions options, Action callback) + { + var rawOptions = options.ToRaw(); + ScreenCapturerCaptureCallbackNativeCallback nativeCallback = (arg0, arg1, delivery, userData) => { try { if (!Interop.native_event_delivery_is_active(delivery)) return; callback(arg0, arg1 == 0 ? null : new Image(Interop.native_handle_retain(arg1))); } catch (Exception) { } finally { Interop.native_event_delivery_complete(delivery, true); } }; + Interop.native_screen_capturer_capture(source?.NativeHandle ?? 0, rawOptions, nativeCallback, CallbackKeeper.Hold(nativeCallback), CallbackKeeper.Release); + ScreenCaptureOptions.ReleaseRaw(ref rawOptions); + } + +} + diff --git a/bindings/csharp/src/NativeAPI/ScreenRecorder.cs b/bindings/csharp/src/NativeAPI/ScreenRecorder.cs new file mode 100644 index 00000000..8b8ee5c4 --- /dev/null +++ b/bindings/csharp/src/NativeAPI/ScreenRecorder.cs @@ -0,0 +1,205 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using CNativeAPI; + +namespace NativeAPI; + +public enum ScreenRecordingState +{ + Idle = 0, + Starting = 1, + Recording = 2, + Stopping = 3, + Completed = 4, + Cancelled = 5, + Failed = 6, +} + +public struct ScreenRecordingOptions +{ + public ScreenCaptureOptions Capture; + public double FrameRate; + public ulong VideoBitrate; + + public ScreenRecordingOptions(ScreenCaptureOptions capture, double frameRate, ulong videoBitrate) + { + Capture = capture; + FrameRate = frameRate; + VideoBitrate = videoBitrate; + } + + internal static ScreenRecordingOptions FromRaw(in native_screen_recording_options_t raw) + { + return new ScreenRecordingOptions(ScreenCaptureOptions.FromRaw(in raw.capture), raw.frame_rate, (ulong)raw.video_bitrate.Value); + } + + internal native_screen_recording_options_t ToRaw() + { + var raw = new native_screen_recording_options_t(); + raw.capture = Capture.ToRaw(); + raw.frame_rate = FrameRate; + raw.video_bitrate = new CULong(checked((nuint)VideoBitrate)); + return raw; + } + + internal static void ReleaseRaw(ref native_screen_recording_options_t raw) + { + } +} + +/// One ScreenRecordingEvent, in its concrete form. +public abstract record ScreenRecordingEvent +{ + private ScreenRecordingEvent() { } + + public sealed record Started : ScreenRecordingEvent; + public sealed record Completed(string? FilePath, double Duration) : ScreenRecordingEvent; + public sealed record Cancelled : ScreenRecordingEvent; + public sealed record Failed(string? Message) : ScreenRecordingEvent; + + internal static ScreenRecordingEvent? FromRaw(in native_screen_recording_event_t raw) + { + switch (raw.type) + { + case 0: return new Started(); + case 1: return new Completed(Marshal.PtrToStringUTF8(raw.data.completed.file_path), raw.data.completed.duration); + case 2: return new Cancelled(); + case 3: return new Failed(Marshal.PtrToStringUTF8(raw.data.failed.message)); + default: return null; + } + } +} + +/// Owned handle to a native ScreenRecorder. +public sealed partial class ScreenRecorder : IDisposable +{ + public ulong NativeHandle { get; private set; } + private readonly bool _ownsHandle; + + public ScreenRecorder(ulong nativeHandle, bool ownsHandle = true) + { + NativeHandle = nativeHandle; + _ownsHandle = ownsHandle; + } + + ~ScreenRecorder() => ReleaseHandle(); + + public void Dispose() + { + ReleaseHandle(); + GC.SuppressFinalize(this); + } + + private void ReleaseHandle() + { + if (_ownsHandle && NativeHandle != 0) + { + Interop.native_screen_recorder_free(NativeHandle); + NativeHandle = 0; + } + } + + /// Creates a new ScreenRecorder; returns null if the native side failed. + public static ScreenRecorder? Create() + { + var handle = Interop.native_screen_recorder_create(); + return handle == 0 ? null : new ScreenRecorder(handle); + } + + public static bool IsSupported() + { + var rawResult = Interop.native_screen_recorder_is_supported(); + return rawResult; + } + + public bool Start(CaptureSource? source, string filePath, ScreenRecordingOptions options) + { + var rawOptions = options.ToRaw(); + var rawResult = Interop.native_screen_recorder_start(NativeHandle, source?.NativeHandle ?? 0, filePath, rawOptions); + ScreenRecordingOptions.ReleaseRaw(ref rawOptions); + return rawResult; + } + + public bool Stop() + { + var rawResult = Interop.native_screen_recorder_stop(NativeHandle); + return rawResult; + } + + public bool Cancel() + { + var rawResult = Interop.native_screen_recorder_cancel(NativeHandle); + return rawResult; + } + + public ScreenRecordingState State + { + get + { + var rawResult = Interop.native_screen_recorder_get_state(NativeHandle); + return (ScreenRecordingState)rawResult; + } + } + + public bool IsActive + { + get + { + var rawResult = Interop.native_screen_recorder_is_active(NativeHandle); + return rawResult; + } + } + + public double Duration + { + get + { + var rawResult = Interop.native_screen_recorder_get_duration(NativeHandle); + return rawResult; + } + } + + public string? FilePath + { + get + { + var rawResult = Interop.native_screen_recorder_get_file_path(NativeHandle); + return Interop.ConsumeString(rawResult); + } + } + + /// Registers for every ScreenRecordingEvent this ScreenRecorder emits. + /// + /// The delegate is kept alive until the listener is removed or its emitter + /// destroyed; the core releases it then. + /// + public ulong AddListener(Action callback) + { + ScreenRecordingEventNativeCallback native = (evt, userData) => + { + if (evt == IntPtr.Zero) + { + return; + } + var value = ScreenRecordingEvent.FromRaw(Marshal.PtrToStructure(evt)); + if (value is not null) + { + callback(value); + } + }; + return Interop.native_screen_recorder_add_listener(NativeHandle, native, CallbackKeeper.Hold(native), CallbackKeeper.Release); + } + + /// Unregisters a listener. Returns false if unknown. + public bool RemoveListener(ulong listenerId) + { + return Interop.native_screen_recorder_remove_listener(NativeHandle, listenerId); + } + +} + diff --git a/bindings/dart/cnativeapi/ffigen.yaml b/bindings/dart/cnativeapi/ffigen.yaml index 7990fe65..c83035b7 100644 --- a/bindings/dart/cnativeapi/ffigen.yaml +++ b/bindings/dart/cnativeapi/ffigen.yaml @@ -13,6 +13,7 @@ headers: - "../../../core/src/capi/accessibility_manager_c.h" - "../../../core/src/capi/app_info_c.h" - "../../../core/src/capi/application_c.h" + - "../../../core/src/capi/capture_source_c.h" - "../../../core/src/capi/clipboard_c.h" - "../../../core/src/capi/color_c.h" - "../../../core/src/capi/common_c.h" @@ -35,6 +36,8 @@ headers: - "../../../core/src/capi/placement_c.h" - "../../../core/src/capi/positioning_strategy_c.h" - "../../../core/src/capi/preferences_c.h" + - "../../../core/src/capi/screen_capturer_c.h" + - "../../../core/src/capi/screen_recorder_c.h" - "../../../core/src/capi/secure_storage_c.h" - "../../../core/src/capi/shortcut_c.h" - "../../../core/src/capi/shortcut_manager_c.h" @@ -53,6 +56,7 @@ headers: - "../../../core/src/capi/accessibility_manager_c.h" - "../../../core/src/capi/app_info_c.h" - "../../../core/src/capi/application_c.h" + - "../../../core/src/capi/capture_source_c.h" - "../../../core/src/capi/clipboard_c.h" - "../../../core/src/capi/color_c.h" - "../../../core/src/capi/common_c.h" @@ -75,6 +79,8 @@ headers: - "../../../core/src/capi/placement_c.h" - "../../../core/src/capi/positioning_strategy_c.h" - "../../../core/src/capi/preferences_c.h" + - "../../../core/src/capi/screen_capturer_c.h" + - "../../../core/src/capi/screen_recorder_c.h" - "../../../core/src/capi/secure_storage_c.h" - "../../../core/src/capi/shortcut_c.h" - "../../../core/src/capi/shortcut_manager_c.h" diff --git a/bindings/dart/cnativeapi/hook/build.dart b/bindings/dart/cnativeapi/hook/build.dart index fa43a17c..412889d4 100644 --- a/bindings/dart/cnativeapi/hook/build.dart +++ b/bindings/dart/cnativeapi/hook/build.dart @@ -59,6 +59,7 @@ void main(List args) async { switch (targetOS) { case OS.macOS: flags.addAll(_objectiveCpp); + flags.addAll(['-weak_framework', 'ScreenCaptureKit']); frameworks = [ 'Cocoa', 'QuartzCore', @@ -66,6 +67,10 @@ void main(List args) async { 'CoreGraphics', 'ApplicationServices', 'ServiceManagement', + 'AVFoundation', + 'CoreMedia', + 'CoreVideo', + 'VideoToolbox', ]; case OS.iOS: flags.addAll(_objectiveCpp); @@ -99,6 +104,14 @@ void main(List args) async { 'advapi32', 'version', 'gdi32', + 'd3d11', + 'dxgi', + 'windowsapp', + 'runtimeobject', + 'mfplat', + 'mfreadwrite', + 'mfuuid', + 'windowscodecs', ]); } diff --git a/bindings/dart/cnativeapi/lib/src/bindings_generated.dart b/bindings/dart/cnativeapi/lib/src/bindings_generated.dart index e84afe46..6a1e097a 100644 --- a/bindings/dart/cnativeapi/lib/src/bindings_generated.dart +++ b/bindings/dart/cnativeapi/lib/src/bindings_generated.dart @@ -174,6 +174,46 @@ external ffi.Pointer native_button_get_text(int button); @ffi.Native)>() external void native_button_set_text(int button, ffi.Pointer text); +/// Releases the caller's reference. Safe to call with an invalid or +/// already-released handle. +@ffi.Native() +external void native_capture_source_free(int capture_source); + +/// Caller owns the returned handle; release it with native_capture_source_free(). +@ffi.Native() +external int native_capture_source_from_display(int display); + +/// Caller owns the returned handle; release it with native_capture_source_free(). +@ffi.Native() +external int native_capture_source_from_window(int window); + +/// Caller owns the returned string; free it with free_c_str(). +@ffi.Native Function(native_capture_source_t)>() +external ffi.Pointer native_capture_source_get_name( + int capture_source, +); + +@ffi.Native( + symbol: 'native_capture_source_get_type', +) +external int _native_capture_source_get_type(int capture_source); + +native_capture_source_type_t native_capture_source_get_type( + Dartnative_capture_source_t capture_source, +) { + return native_capture_source_type_t.fromValue( + _native_capture_source_get_type(capture_source), + ); +} + +/// Frees everything the struct owns. +@ffi.Native< + ffi.Void Function(ffi.Pointer) +>() +external void native_capture_source_selection_options_free( + ffi.Pointer value, +); + /// Registers @p callback for every ClipboardEvent this Clipboard emits. /// @return the listener id, or NATIVE_INVALID_LISTENER_ID on failure. @ffi.Native< @@ -309,6 +349,10 @@ external bool native_clipboard_write_image(int image); @ffi.Native)>() external bool native_clipboard_write_text(ffi.Pointer text); +/// Frees everything the struct owns. +@ffi.Native)>() +external void native_color_free(ffi.Pointer value); + @ffi.Native)>() external native_color_t native_color_from_hex(ffi.Pointer hex); @@ -589,7 +633,7 @@ external void native_drag_source_set_text( ffi.Pointer text, ); -@ffi.Native() +@ffi.Native() external bool native_drag_source_start_dragging(int drag_source, int window); /// Registers @p callback for every DropTargetEvent this DropTarget emits. @@ -628,7 +672,7 @@ external int native_drop_target_add_listener_async( ); /// Creates a DropTarget instance; release it with native_drop_target_free(). -@ffi.Native() +@ffi.Native() external int native_drop_target_create(int window); /// Releases the caller's reference. Safe to call with an invalid or @@ -683,6 +727,10 @@ void native_drop_target_set_drop_operation( @ffi.Native() external native_edge_insets_t native_edge_insets_all(double value); +/// Frees everything the struct owns. +@ffi.Native)>() +external void native_edge_insets_free(ffi.Pointer value); + @ffi.Native() external native_edge_insets_t native_edge_insets_symmetric( double vertical, @@ -812,7 +860,7 @@ void native_file_dialog_set_modality( return _native_file_dialog_set_modality(file_dialog, modality.value); } -@ffi.Native() +@ffi.Native() external bool native_file_dialog_set_parent_window(int file_dialog, int window); @ffi.Native)>() @@ -1582,7 +1630,7 @@ void native_message_dialog_set_modality( return _native_message_dialog_set_modality(message_dialog, modality.value); } -@ffi.Native() +@ffi.Native() external bool native_message_dialog_set_parent_window( int message_dialog, int window, @@ -1666,6 +1714,10 @@ external bool native_notification_manager_show( @ffi.Native() external void native_notification_manager_shutdown(); +/// Frees everything the struct owns. +@ffi.Native)>() +external void native_point_free(ffi.Pointer value); + /// Caller owns the returned handle; release it with native_positioning_strategy_free(). @ffi.Native() external int native_positioning_strategy_absolute(native_point_t point); @@ -1799,6 +1851,171 @@ external bool native_preferences_set( ffi.Pointer value, ); +/// Frees everything the struct owns. +@ffi.Native)>() +external void native_rectangle_free(ffi.Pointer value); + +/// Frees everything the struct owns. +@ffi.Native)>() +external void native_screen_capture_options_free( + ffi.Pointer value, +); + +@ffi.Native< + ffi.Void Function( + native_capture_source_t$1, + native_screen_capture_options_t, + native_screen_capturer_capture_callback_t, + ffi.Pointer, + native_release_user_data_t, + ) +>() +external void native_screen_capturer_capture( + int source, + native_screen_capture_options_t options, + native_screen_capturer_capture_callback_t callback, + ffi.Pointer callback_user_data, + native_release_user_data_t callback_release_user_data, +); + +@ffi.Native( + symbol: 'native_screen_capturer_is_direct_capture_supported', +) +external bool _native_screen_capturer_is_direct_capture_supported(int type); + +bool native_screen_capturer_is_direct_capture_supported( + native_capture_source_type_t type, +) { + return _native_screen_capturer_is_direct_capture_supported(type.value); +} + +@ffi.Native() +external bool native_screen_capturer_is_source_selection_supported(); + +@ffi.Native() +external bool native_screen_capturer_is_supported(); + +@ffi.Native< + ffi.Void Function( + native_capture_source_selection_options_t, + native_screen_capturer_select_source_callback_t, + ffi.Pointer, + native_release_user_data_t, + ) +>() +external void native_screen_capturer_select_source( + native_capture_source_selection_options_t options, + native_screen_capturer_select_source_callback_t callback, + ffi.Pointer callback_user_data, + native_release_user_data_t callback_release_user_data, +); + +/// Registers @p callback for every ScreenRecordingEvent this ScreenRecorder emits. +/// @return the listener id, or NATIVE_INVALID_LISTENER_ID on failure. +@ffi.Native< + native_listener_id_t Function( + native_screen_recorder_t, + native_screen_recording_event_callback_t, + ffi.Pointer, + native_release_user_data_t, + ) +>() +external int native_screen_recorder_add_listener( + int screen_recorder, + native_screen_recording_event_callback_t callback, + ffi.Pointer user_data, + native_release_user_data_t release_user_data, +); + +/// Registers an asynchronous callback. Its event, borrowed handles and user_data +/// remain valid until event_delivery_complete is called, including after removal. +/// Every delivered payload must be acknowledged. Check is_active before invoking a queued callback. +@ffi.Native< + native_listener_id_t Function( + native_screen_recorder_t, + native_screen_recording_event_callback_t_async, + ffi.Pointer, + native_release_user_data_t, + ) +>() +external int native_screen_recorder_add_listener_async( + int screen_recorder, + native_screen_recording_event_callback_t_async callback, + ffi.Pointer user_data, + native_release_user_data_t release_user_data, +); + +@ffi.Native() +external bool native_screen_recorder_cancel(int screen_recorder); + +/// Creates a ScreenRecorder instance; release it with native_screen_recorder_free(). +@ffi.Native() +external int native_screen_recorder_create(); + +/// Releases the caller's reference. Safe to call with an invalid or +/// already-released handle. +@ffi.Native() +external void native_screen_recorder_free(int screen_recorder); + +@ffi.Native() +external double native_screen_recorder_get_duration(int screen_recorder); + +/// Caller owns the returned string; free it with free_c_str(). +@ffi.Native Function(native_screen_recorder_t)>() +external ffi.Pointer native_screen_recorder_get_file_path( + int screen_recorder, +); + +@ffi.Native( + symbol: 'native_screen_recorder_get_state', +) +external int _native_screen_recorder_get_state(int screen_recorder); + +native_screen_recording_state_t native_screen_recorder_get_state( + Dartnative_screen_recorder_t screen_recorder, +) { + return native_screen_recording_state_t.fromValue( + _native_screen_recorder_get_state(screen_recorder), + ); +} + +@ffi.Native() +external bool native_screen_recorder_is_active(int screen_recorder); + +@ffi.Native() +external bool native_screen_recorder_is_supported(); + +/// Unregisters a listener. Returns false if unknown. +@ffi.Native() +external bool native_screen_recorder_remove_listener( + int screen_recorder, + int listener_id, +); + +@ffi.Native< + ffi.Bool Function( + native_screen_recorder_t, + native_capture_source_t$2, + ffi.Pointer, + native_screen_recording_options_t, + ) +>() +external bool native_screen_recorder_start( + int screen_recorder, + int source, + ffi.Pointer file_path, + native_screen_recording_options_t options, +); + +@ffi.Native() +external bool native_screen_recorder_stop(int screen_recorder); + +/// Frees everything the struct owns. +@ffi.Native)>() +external void native_screen_recording_options_free( + ffi.Pointer value, +); + @ffi.Native() external bool native_secure_storage_clear(int secure_storage); @@ -2099,6 +2316,10 @@ external void native_shortcut_set_description( @ffi.Native() external void native_shortcut_set_enabled(int shortcut, bool enabled); +/// Frees everything the struct owns. +@ffi.Native)>() +external void native_size_free(ffi.Pointer value); + /// Free a string list allocated by to_c_string_list /// @param list The list to free (can be nullptr) @ffi.Native)>() @@ -2286,7 +2507,7 @@ native_context_menu_trigger_t native_tray_icon_get_context_menu_trigger( } /// Caller owns the returned handle; release it with native_image_free(). -@ffi.Native() +@ffi.Native() external int native_tray_icon_get_icon(int tray_icon); @ffi.Native( @@ -2372,7 +2593,7 @@ void native_tray_icon_set_context_menu_trigger( return _native_tray_icon_set_context_menu_trigger(tray_icon, trigger.value); } -@ffi.Native() +@ffi.Native() external void native_tray_icon_set_icon(int tray_icon, int image); @ffi.Native( @@ -2821,7 +3042,7 @@ external bool native_window_drag_session_remove_listener( @ffi.Native< ffi.Bool Function( native_window_drag_session_t, - native_window_t$5, + native_window_t$7, native_point_t, ) >() @@ -3114,18 +3335,18 @@ external bool native_window_manager_call_original_hide(int id); external bool native_window_manager_call_original_show(int id); /// Caller owns the returned handle; release it with native_window_free(). -@ffi.Native() +@ffi.Native() external int native_window_manager_get(int id); @ffi.Native() external native_window_list_t native_window_manager_get_all(); /// Caller owns the returned handle; release it with native_window_free(). -@ffi.Native() +@ffi.Native() external int native_window_manager_get_current(); /// Caller owns the returned handle; release it with native_window_free(). -@ffi.Native() +@ffi.Native() external int native_window_manager_get_window_at_point( native_point_t point, int excluded_window_id, @@ -3478,6 +3699,8 @@ external void native_window_unmaximize(int window); const int NATIVE_INVALID_BUTTON = 0; +const int NATIVE_INVALID_CAPTURE_SOURCE = 0; + const int NATIVE_INVALID_DISPLAY = 0; const int NATIVE_INVALID_DRAG_SOURCE = 0; @@ -3512,6 +3735,8 @@ const int NATIVE_INVALID_POSITIONING_STRATEGY = 0; const int NATIVE_INVALID_PREFERENCES = 0; +const int NATIVE_INVALID_SCREEN_RECORDER = 0; + const int NATIVE_INVALID_SECURE_STORAGE = 0; const int NATIVE_INVALID_SHORTCUT = 0; @@ -3560,32 +3785,36 @@ final class UnnamedStruct$10 extends ffi.Struct { } final class UnnamedStruct$11 extends ffi.Struct { - external ffi.Pointer error_message; + external ffi.Pointer file_path; + + @ffi.Double() + external double duration; static ffi.Pointer $allocate( ffi.Allocator $allocator, { - required ffi.Pointer error_message, - }) => $allocator()..ref.error_message = error_message; + required ffi.Pointer file_path, + required double duration, + }) => $allocator() + ..ref.file_path = file_path + ..ref.duration = duration; } final class UnnamedStruct$12 extends ffi.Struct { - @native_tray_icon_id_t() - external int tray_icon_id; + external ffi.Pointer message; static ffi.Pointer $allocate( ffi.Allocator $allocator, { - required int tray_icon_id, - }) => $allocator()..ref.tray_icon_id = tray_icon_id; + required ffi.Pointer message, + }) => $allocator()..ref.message = message; } final class UnnamedStruct$13 extends ffi.Struct { - @native_tray_icon_id_t() - external int tray_icon_id; + external ffi.Pointer error_message; static ffi.Pointer $allocate( ffi.Allocator $allocator, { - required int tray_icon_id, - }) => $allocator()..ref.tray_icon_id = tray_icon_id; + required ffi.Pointer error_message, + }) => $allocator()..ref.error_message = error_message; } final class UnnamedStruct$14 extends ffi.Struct { @@ -3599,44 +3828,40 @@ final class UnnamedStruct$14 extends ffi.Struct { } final class UnnamedStruct$15 extends ffi.Struct { - external ffi.Pointer text; + @native_tray_icon_id_t() + external int tray_icon_id; static ffi.Pointer $allocate( ffi.Allocator $allocator, { - required ffi.Pointer text, - }) => $allocator()..ref.text = text; + required int tray_icon_id, + }) => $allocator()..ref.tray_icon_id = tray_icon_id; } final class UnnamedStruct$16 extends ffi.Struct { - external native_point_t new_position; + @native_tray_icon_id_t() + external int tray_icon_id; + + static ffi.Pointer $allocate( + ffi.Allocator $allocator, { + required int tray_icon_id, + }) => $allocator()..ref.tray_icon_id = tray_icon_id; } final class UnnamedStruct$17 extends ffi.Struct { - external native_size_t new_size; + external ffi.Pointer text; + + static ffi.Pointer $allocate( + ffi.Allocator $allocator, { + required ffi.Pointer text, + }) => $allocator()..ref.text = text; } final class UnnamedStruct$18 extends ffi.Struct { - @native_event_request_t() - external int request; - - static ffi.Pointer $allocate( - ffi.Allocator $allocator, { - required int request, - }) => $allocator()..ref.request = request; + external native_point_t new_position; } final class UnnamedStruct$19 extends ffi.Struct { - @ffi.UnsignedInt() - external int propertyAsInt; - - native_window_property_t get property => - native_window_property_t.fromValue(propertyAsInt); - set property(native_window_property_t value) => propertyAsInt = value.value; - - static ffi.Pointer $allocate( - ffi.Allocator $allocator, { - required native_window_property_t property, - }) => $allocator()..ref.property = property; + external native_size_t new_size; } final class UnnamedStruct$2 extends ffi.Struct { @@ -3654,6 +3879,30 @@ final class UnnamedStruct$2 extends ffi.Struct { } final class UnnamedStruct$20 extends ffi.Struct { + @native_event_request_t() + external int request; + + static ffi.Pointer $allocate( + ffi.Allocator $allocator, { + required int request, + }) => $allocator()..ref.request = request; +} + +final class UnnamedStruct$21 extends ffi.Struct { + @ffi.UnsignedInt() + external int propertyAsInt; + + native_window_property_t get property => + native_window_property_t.fromValue(propertyAsInt); + set property(native_window_property_t value) => propertyAsInt = value.value; + + static ffi.Pointer $allocate( + ffi.Allocator $allocator, { + required native_window_property_t property, + }) => $allocator()..ref.property = property; +} + +final class UnnamedStruct$22 extends ffi.Struct { @ffi.UnsignedInt() external int occlusion_stateAsInt; @@ -3662,10 +3911,10 @@ final class UnnamedStruct$20 extends ffi.Struct { set occlusion_state(native_window_occlusion_state_t value) => occlusion_stateAsInt = value.value; - static ffi.Pointer $allocate( + static ffi.Pointer $allocate( ffi.Allocator $allocator, { required native_window_occlusion_state_t occlusion_state, - }) => $allocator()..ref.occlusion_state = occlusion_state; + }) => $allocator()..ref.occlusion_state = occlusion_state; } final class UnnamedStruct$3 extends ffi.Struct { @@ -3744,6 +3993,18 @@ final class UnnamedUnion$1 extends ffi.Union { external UnnamedStruct$2 ended; } +final class UnnamedUnion$10 extends ffi.Union { + external UnnamedStruct$18 moved; + + external UnnamedStruct$19 resized; + + external UnnamedStruct$20 close_requested; + + external UnnamedStruct$21 property_changed; + + external UnnamedStruct$22 occlusion_changed; +} + final class UnnamedUnion$2 extends ffi.Union { external UnnamedStruct$3 dropped; } @@ -3769,31 +4030,25 @@ final class UnnamedUnion$5 extends ffi.Union { } final class UnnamedUnion$6 extends ffi.Union { - external UnnamedStruct$11 registration_failed; + external UnnamedStruct$11 completed; + + external UnnamedStruct$12 failed; } final class UnnamedUnion$7 extends ffi.Union { - external UnnamedStruct$12 clicked; - - external UnnamedStruct$13 right_clicked; - - external UnnamedStruct$14 double_clicked; + external UnnamedStruct$13 registration_failed; } final class UnnamedUnion$8 extends ffi.Union { - external UnnamedStruct$15 text_field_changed; -} - -final class UnnamedUnion$9 extends ffi.Union { - external UnnamedStruct$16 moved; + external UnnamedStruct$14 clicked; - external UnnamedStruct$17 resized; + external UnnamedStruct$15 right_clicked; - external UnnamedStruct$18 close_requested; - - external UnnamedStruct$19 property_changed; + external UnnamedStruct$16 double_clicked; +} - external UnnamedStruct$20 occlusion_changed; +final class UnnamedUnion$9 extends ffi.Union { + external UnnamedStruct$17 text_field_changed; } typedef native_application_event_callback_t = @@ -3890,6 +4145,60 @@ enum native_brightness_t { typedef native_button_t = ffi.Uint64; typedef Dartnative_button_t = int; +final class native_capture_source_selection_options_t extends ffi.Struct { + @ffi.Bool() + external bool allow_displays; + + @ffi.Bool() + external bool allow_windows; + + @native_window_t$2() + external int parent_window; + + static ffi.Pointer $allocate( + ffi.Allocator $allocator, { + required bool allow_displays, + required bool allow_windows, + required int parent_window, + }) => $allocator() + ..ref.allow_displays = allow_displays + ..ref.allow_windows = allow_windows + ..ref.parent_window = parent_window; +} + +/// Opaque CaptureSource handle. +/// +/// A generational index into the library's handle table, NOT a pointer: +/// never dereference it, and compare it against NATIVE_INVALID_CAPTURE_SOURCE rather than NULL. +/// Releasing a handle invalidates it; later calls fail safely instead of +/// touching freed memory. +typedef native_capture_source_t = ffi.Uint64; +typedef Dartnative_capture_source_t = int; +typedef native_capture_source_t$1 = ffi.Uint64; +typedef Dartnative_capture_source_t$1 = int; +typedef native_capture_source_t$2 = ffi.Uint64; +typedef Dartnative_capture_source_t$2 = int; + +enum native_capture_source_type_t { + NATIVE_CAPTURE_SOURCE_TYPE_NONE(0), + NATIVE_CAPTURE_SOURCE_TYPE_DISPLAY(1), + NATIVE_CAPTURE_SOURCE_TYPE_WINDOW(2), + NATIVE_CAPTURE_SOURCE_TYPE_UNKNOWN(3); + + final int value; + const native_capture_source_type_t(this.value); + + static native_capture_source_type_t fromValue(int value) => switch (value) { + 0 => NATIVE_CAPTURE_SOURCE_TYPE_NONE, + 1 => NATIVE_CAPTURE_SOURCE_TYPE_DISPLAY, + 2 => NATIVE_CAPTURE_SOURCE_TYPE_WINDOW, + 3 => NATIVE_CAPTURE_SOURCE_TYPE_UNKNOWN, + _ => throw ArgumentError( + 'Unknown value for native_capture_source_type_t: $value', + ), + }; +} + final class native_clipboard_data_t extends ffi.Struct { external ffi.Pointer text; @@ -4228,6 +4537,8 @@ typedef native_display_t = ffi.Uint64; typedef Dartnative_display_t = int; typedef native_display_t$1 = ffi.Uint64; typedef Dartnative_display_t$1 = int; +typedef native_display_t$2 = ffi.Uint64; +typedef Dartnative_display_t$2 = int; enum native_drag_operation_t { NATIVE_DRAG_OPERATION_NONE(0), @@ -4517,6 +4828,8 @@ typedef native_image_t$4 = ffi.Uint64; typedef Dartnative_image_t$4 = int; typedef native_image_t$5 = ffi.Uint64; typedef Dartnative_image_t$5 = int; +typedef native_image_t$6 = ffi.Uint64; +typedef Dartnative_image_t$6 = int; /// Opaque ImageView handle. /// @@ -5076,6 +5389,168 @@ enum native_resize_edge_t { }; } +final class native_screen_capture_options_t extends ffi.Struct { + @ffi.Bool() + external bool include_cursor; + + @ffi.Bool() + external bool crop_enabled; + + external native_rectangle_t crop; +} + +/// Arguments are borrowed until event_delivery_complete(delivery, ...), which is required exactly once. +typedef native_screen_capturer_capture_callback_t = + ffi.Pointer< + ffi.NativeFunction + >; +typedef native_screen_capturer_capture_callback_tFunction = ffi.Void Function( + ffi.Bool arg0, + native_image_t$3 arg1, + native_event_delivery_t delivery, + ffi.Pointer user_data, +); +typedef Dartnative_screen_capturer_capture_callback_tFunction = void Function( + bool arg0, + Dartnative_image_t$3 arg1, + Dartnative_event_delivery_t delivery, + ffi.Pointer user_data, +); + +/// Arguments are borrowed until event_delivery_complete(delivery, ...), which is required exactly once. +typedef native_screen_capturer_select_source_callback_t = + ffi.Pointer< + ffi.NativeFunction< + native_screen_capturer_select_source_callback_tFunction + > + >; +typedef native_screen_capturer_select_source_callback_tFunction = + ffi.Void Function( + ffi.Bool arg0, + native_capture_source_t$1 arg1, + native_event_delivery_t delivery, + ffi.Pointer user_data, + ); +typedef Dartnative_screen_capturer_select_source_callback_tFunction = + void Function( + bool arg0, + Dartnative_capture_source_t$1 arg1, + Dartnative_event_delivery_t delivery, + ffi.Pointer user_data, + ); + +/// Opaque ScreenRecorder handle. +/// +/// A generational index into the library's handle table, NOT a pointer: +/// never dereference it, and compare it against NATIVE_INVALID_SCREEN_RECORDER rather than NULL. +/// Releasing a handle invalidates it; later calls fail safely instead of +/// touching freed memory. +typedef native_screen_recorder_t = ffi.Uint64; +typedef Dartnative_screen_recorder_t = int; +typedef native_screen_recording_event_callback_t = + ffi.Pointer< + ffi.NativeFunction + >; +typedef native_screen_recording_event_callback_tFunction = ffi.Void Function( + ffi.Pointer event, + ffi.Pointer user_data, +); +typedef Dartnative_screen_recording_event_callback_tFunction = void Function( + ffi.Pointer event, + ffi.Pointer user_data, +); +typedef native_screen_recording_event_callback_t_async = + ffi.Pointer< + ffi.NativeFunction + >; +typedef native_screen_recording_event_callback_t_asyncFunction = + ffi.Void Function( + ffi.Pointer event, + native_event_delivery_t delivery, + ffi.Pointer user_data, + ); +typedef Dartnative_screen_recording_event_callback_t_asyncFunction = + void Function( + ffi.Pointer event, + Dartnative_event_delivery_t delivery, + ffi.Pointer user_data, + ); + +/// One ScreenRecordingEvent, tagged by its concrete type. +/// +/// Synchronous callbacks borrow this payload until they return. Async callbacks +/// borrow it until event_delivery_complete. Copy anything needed after that. +final class native_screen_recording_event_t extends ffi.Struct { + @ffi.UnsignedInt() + external int typeAsInt; + + native_screen_recording_event_type_t get type => + native_screen_recording_event_type_t.fromValue(typeAsInt); + set type(native_screen_recording_event_type_t value) => + typeAsInt = value.value; + + external UnnamedUnion$6 data; +} + +/// Which concrete ScreenRecordingEvent arrived. +enum native_screen_recording_event_type_t { + NATIVE_SCREEN_RECORDING_EVENT_TYPE_STARTED(0), + NATIVE_SCREEN_RECORDING_EVENT_TYPE_COMPLETED(1), + NATIVE_SCREEN_RECORDING_EVENT_TYPE_CANCELLED(2), + NATIVE_SCREEN_RECORDING_EVENT_TYPE_FAILED(3); + + final int value; + const native_screen_recording_event_type_t(this.value); + + static native_screen_recording_event_type_t fromValue(int value) => + switch (value) { + 0 => NATIVE_SCREEN_RECORDING_EVENT_TYPE_STARTED, + 1 => NATIVE_SCREEN_RECORDING_EVENT_TYPE_COMPLETED, + 2 => NATIVE_SCREEN_RECORDING_EVENT_TYPE_CANCELLED, + 3 => NATIVE_SCREEN_RECORDING_EVENT_TYPE_FAILED, + _ => throw ArgumentError( + 'Unknown value for native_screen_recording_event_type_t: $value', + ), + }; +} + +final class native_screen_recording_options_t extends ffi.Struct { + external native_screen_capture_options_t capture; + + @ffi.Double() + external double frame_rate; + + @ffi.UnsignedLong() + external int video_bitrate; +} + +enum native_screen_recording_state_t { + NATIVE_SCREEN_RECORDING_STATE_IDLE(0), + NATIVE_SCREEN_RECORDING_STATE_STARTING(1), + NATIVE_SCREEN_RECORDING_STATE_RECORDING(2), + NATIVE_SCREEN_RECORDING_STATE_STOPPING(3), + NATIVE_SCREEN_RECORDING_STATE_COMPLETED(4), + NATIVE_SCREEN_RECORDING_STATE_CANCELLED(5), + NATIVE_SCREEN_RECORDING_STATE_FAILED(6); + + final int value; + const native_screen_recording_state_t(this.value); + + static native_screen_recording_state_t fromValue(int value) => + switch (value) { + 0 => NATIVE_SCREEN_RECORDING_STATE_IDLE, + 1 => NATIVE_SCREEN_RECORDING_STATE_STARTING, + 2 => NATIVE_SCREEN_RECORDING_STATE_RECORDING, + 3 => NATIVE_SCREEN_RECORDING_STATE_STOPPING, + 4 => NATIVE_SCREEN_RECORDING_STATE_COMPLETED, + 5 => NATIVE_SCREEN_RECORDING_STATE_CANCELLED, + 6 => NATIVE_SCREEN_RECORDING_STATE_FAILED, + _ => throw ArgumentError( + 'Unknown value for native_screen_recording_state_t: $value', + ), + }; +} + /// Opaque SecureStorage handle. /// /// A generational index into the library's handle table, NOT a pointer: @@ -5136,7 +5611,7 @@ final class native_shortcut_event_t extends ffi.Struct { external ffi.Pointer accelerator; - external UnnamedUnion$6 data; + external UnnamedUnion$7 data; } /// Which concrete ShortcutEvent arrived. @@ -5416,7 +5891,7 @@ final class native_tray_icon_event_t extends ffi.Struct { native_tray_icon_event_type_t.fromValue(typeAsInt); set type(native_tray_icon_event_type_t value) => typeAsInt = value.value; - external UnnamedUnion$7 data; + external UnnamedUnion$8 data; } /// Which concrete TrayIconEvent arrived. @@ -5605,7 +6080,7 @@ final class native_view_event_t extends ffi.Struct { @native_view_id_t() external int view_id; - external UnnamedUnion$8 data; + external UnnamedUnion$9 data; } /// Which concrete ViewEvent arrived. @@ -5840,7 +6315,7 @@ final class native_window_event_t extends ffi.Struct { @native_window_id_t() external int window_id; - external UnnamedUnion$9 data; + external UnnamedUnion$10 data; } /// Which concrete WindowEvent arrived. @@ -6009,6 +6484,8 @@ typedef native_window_t = ffi.Uint64; typedef Dartnative_window_t = int; typedef native_window_t$1 = ffi.Uint64; typedef Dartnative_window_t$1 = int; +typedef native_window_t$10 = ffi.Uint64; +typedef Dartnative_window_t$10 = int; typedef native_window_t$2 = ffi.Uint64; typedef Dartnative_window_t$2 = int; typedef native_window_t$3 = ffi.Uint64; @@ -6023,3 +6500,5 @@ typedef native_window_t$7 = ffi.Uint64; typedef Dartnative_window_t$7 = int; typedef native_window_t$8 = ffi.Uint64; typedef Dartnative_window_t$8 = int; +typedef native_window_t$9 = ffi.Uint64; +typedef Dartnative_window_t$9 = int; diff --git a/bindings/dart/nativeapi/CHANGELOG.md b/bindings/dart/nativeapi/CHANGELOG.md index 78c6e2d1..1aae895b 100644 --- a/bindings/dart/nativeapi/CHANGELOG.md +++ b/bindings/dart/nativeapi/CHANGELOG.md @@ -1,5 +1,7 @@ ## Unreleased +- Add `ScreenCapturer`, `CaptureSource` and `ScreenRecorder` for screenshots and silent H.264 MP4 recordings on macOS 14+ and Windows 10 version 2004+. Include system source selection, physical-pixel crops, cursor options and recording lifecycle events, plus a Flutter desktop example. Other platforms report unsupported. + - Windows: window observation now runs on the thread that owns the windows, not the thread that first called the API (#18). `WindowManager` installs its WinEvent hooks and window subclassing there, and deferred main-thread work is delivered there, so a Flutter app keeping the default thread policy (UI isolate on its own thread) still gets focus, lifetime, occlusion and geometry events. Out-of-context WinEvents and window subclassing only work on the window-owning thread. - Windows: an elevated application's tray icon now receives clicks (#110). The shell posts the icon's callback message from a lower integrity level, so User Interface Privilege Isolation dropped it at an elevated window; the icon's host window now allows that message. An elevated application also lets the theme-change broadcasts through now, so template icons follow a theme change. diff --git a/bindings/dart/nativeapi/lib/src/capture_source.dart b/bindings/dart/nativeapi/lib/src/capture_source.dart new file mode 100644 index 00000000..c096c6ec --- /dev/null +++ b/bindings/dart/nativeapi/lib/src/capture_source.dart @@ -0,0 +1,92 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +// ignore_for_file: unused_import, unnecessary_import + +import 'dart:ffi' as ffi; + +import 'package:cnativeapi/cnativeapi.dart' as c; +import 'package:ffi/ffi.dart' as pkg_ffi; + +import 'display.dart'; +import 'window.dart'; + +enum CaptureSourceType { + none(0), + display(1), + window(2), + unknown(3); + + const CaptureSourceType(this.value); + final int value; + + static CaptureSourceType fromValue(int value) => switch (value) { + 0 => CaptureSourceType.none, + 1 => CaptureSourceType.display, + 2 => CaptureSourceType.window, + 3 => CaptureSourceType.unknown, + _ => CaptureSourceType.none, + }; + + c.native_capture_source_type_t get raw => + c.native_capture_source_type_t.fromValue(value); +} + +class CaptureSource implements ffi.Finalizable { + /// Adopts a handle returned by the C API and releases it when this + /// object becomes unreachable. + CaptureSource.fromHandle(this.nativeHandle) { + _finalizer.attach( + this, + ffi.Pointer.fromAddress(nativeHandle), + detach: this, + ); + } + + /// Wraps a handle owned elsewhere; releasing it stays the owner's job. + CaptureSource.borrowed(this.nativeHandle); + + /// The underlying handle-table entry. + final int nativeHandle; + + static final _finalizer = ffi.NativeFinalizer( + ffi.Native.addressOf< + ffi.NativeFunction)> + >(c.native_handle_finalize), + ); + + /// Releases the handle now instead of at collection. + void dispose() { + _finalizer.detach(this); + c.native_capture_source_free(nativeHandle); + } + + static CaptureSource? fromDisplay(Display? display) { + final handle = c.native_capture_source_from_display( + display?.nativeHandle ?? 0, + ); + if (handle == 0) return null; + return CaptureSource.fromHandle(handle); + } + + static CaptureSource? fromWindow(Window? window) { + final handle = c.native_capture_source_from_window( + window?.nativeHandle ?? 0, + ); + if (handle == 0) return null; + return CaptureSource.fromHandle(handle); + } + + CaptureSourceType get type { + final raw = c.native_capture_source_get_type(nativeHandle); + return CaptureSourceType.fromValue(raw.value); + } + + String? get name { + final resultPointer = c.native_capture_source_get_name(nativeHandle); + if (resultPointer == ffi.nullptr) return null; + final result = resultPointer.cast().toDartString(); + c.free_c_str(resultPointer); + return result; + } +} diff --git a/bindings/dart/nativeapi/lib/src/generated.dart b/bindings/dart/nativeapi/lib/src/generated.dart index ccccad72..c56c2291 100644 --- a/bindings/dart/nativeapi/lib/src/generated.dart +++ b/bindings/dart/nativeapi/lib/src/generated.dart @@ -4,6 +4,7 @@ export 'accessibility_manager.dart'; export 'app_info.dart'; export 'application.dart'; +export 'capture_source.dart'; export 'clipboard.dart'; export 'device_info.dart'; export 'dialog.dart'; @@ -25,6 +26,8 @@ export 'notification_manager.dart'; export 'placement.dart'; export 'positioning_strategy.dart'; export 'preferences.dart'; +export 'screen_capturer.dart'; +export 'screen_recorder.dart'; export 'secure_storage.dart'; export 'shortcut.dart'; export 'shortcut_manager.dart'; diff --git a/bindings/dart/nativeapi/lib/src/screen_capturer.dart b/bindings/dart/nativeapi/lib/src/screen_capturer.dart new file mode 100644 index 00000000..8f36e919 --- /dev/null +++ b/bindings/dart/nativeapi/lib/src/screen_capturer.dart @@ -0,0 +1,220 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +// ignore_for_file: unused_import, unnecessary_import + +import 'dart:ffi' as ffi; + +import 'package:cnativeapi/cnativeapi.dart' as c; +import 'package:ffi/ffi.dart' as pkg_ffi; + +import 'capture_source.dart'; +import 'foundation/geometry.dart'; +import 'image.dart'; +import 'window.dart'; + +import 'callbacks.dart'; + +class CaptureSourceSelectionOptions { + const CaptureSourceSelectionOptions({ + required this.allowDisplays, + required this.allowWindows, + this.parentWindow, + }); + + final bool allowDisplays; + final bool allowWindows; + final Window? parentWindow; + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is CaptureSourceSelectionOptions && + other.allowDisplays == allowDisplays && + other.allowWindows == allowWindows && + other.parentWindow == parentWindow); + + @override + int get hashCode => Object.hash(allowDisplays, allowWindows, parentWindow); + + @override + String toString() => + 'CaptureSourceSelectionOptions(allowDisplays: $allowDisplays, allowWindows: $allowWindows, parentWindow: $parentWindow)'; + + factory CaptureSourceSelectionOptions.fromNative( + c.native_capture_source_selection_options_t raw, + ) => CaptureSourceSelectionOptions( + allowDisplays: raw.allow_displays, + allowWindows: raw.allow_windows, + parentWindow: raw.parent_window == 0 + ? null + : Window.fromHandle(c.native_handle_retain(raw.parent_window)), + ); + + /// Allocates the C form; free it with [freeNative]. + ffi.Pointer allocNative() { + final pointer = pkg_ffi + .calloc(); + pointer.ref.allow_displays = allowDisplays; + pointer.ref.allow_windows = allowWindows; + pointer.ref.parent_window = parentWindow?.nativeHandle ?? 0; + return pointer; + } + + static void freeNative( + ffi.Pointer pointer, + ) { + pkg_ffi.calloc.free(pointer); + } +} + +class ScreenCaptureOptions { + const ScreenCaptureOptions({ + required this.includeCursor, + required this.cropEnabled, + required this.crop, + }); + + final bool includeCursor; + final bool cropEnabled; + final Rectangle crop; + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ScreenCaptureOptions && + other.includeCursor == includeCursor && + other.cropEnabled == cropEnabled && + other.crop == crop); + + @override + int get hashCode => Object.hash(includeCursor, cropEnabled, crop); + + @override + String toString() => + 'ScreenCaptureOptions(includeCursor: $includeCursor, cropEnabled: $cropEnabled, crop: $crop)'; + + factory ScreenCaptureOptions.fromNative( + c.native_screen_capture_options_t raw, + ) => ScreenCaptureOptions( + includeCursor: raw.include_cursor, + cropEnabled: raw.crop_enabled, + crop: Rectangle.fromNative(raw.crop), + ); + + /// Allocates the C form; free it with [freeNative]. + ffi.Pointer allocNative() { + final pointer = pkg_ffi.calloc(); + pointer.ref.include_cursor = includeCursor; + pointer.ref.crop_enabled = cropEnabled; + final cropPointer = crop.allocNative(); + pointer.ref.crop = cropPointer.ref; + pkg_ffi.calloc.free(cropPointer); + return pointer; + } + + static void freeNative( + ffi.Pointer pointer, + ) { + pkg_ffi.calloc.free(pointer); + } +} + +class ScreenCapturer { + ScreenCapturer._(); + static bool isSupported() { + return c.native_screen_capturer_is_supported(); + } + + static bool isSourceSelectionSupported() { + return c.native_screen_capturer_is_source_selection_supported(); + } + + static bool isDirectCaptureSupported(CaptureSourceType type) { + return c.native_screen_capturer_is_direct_capture_supported(type.raw); + } + + static void selectSource( + CaptureSourceSelectionOptions options, + void Function(bool, CaptureSource?) callback, + ) { + final optionsPointer = options.allocNative(); + final callbackCallable = + ffi.NativeCallable< + ffi.Void Function( + ffi.Bool, + ffi.Uint64, + ffi.Uint64, + ffi.Pointer, + ) + >.listener(( + bool arg0, + int arg1, + int delivery, + ffi.Pointer _, + ) { + try { + if (c.native_event_delivery_is_active(delivery)) { + callback( + arg0, + arg1 == 0 + ? null + : CaptureSource.fromHandle(c.native_handle_retain(arg1)), + ); + } + } finally { + c.native_event_delivery_complete(delivery, true); + } + }); + c.native_screen_capturer_select_source( + optionsPointer.ref, + callbackCallable.nativeFunction, + NativeCallbacks.userData(callbackCallable), + NativeCallbacks.release, + ); + CaptureSourceSelectionOptions.freeNative(optionsPointer); + } + + static void capture( + CaptureSource? source, + ScreenCaptureOptions options, + void Function(bool, Image?) callback, + ) { + final optionsPointer = options.allocNative(); + final callbackCallable = + ffi.NativeCallable< + ffi.Void Function( + ffi.Bool, + ffi.Uint64, + ffi.Uint64, + ffi.Pointer, + ) + >.listener(( + bool arg0, + int arg1, + int delivery, + ffi.Pointer _, + ) { + try { + if (c.native_event_delivery_is_active(delivery)) { + callback( + arg0, + arg1 == 0 + ? null + : Image.fromHandle(c.native_handle_retain(arg1)), + ); + } + } finally { + c.native_event_delivery_complete(delivery, true); + } + }); + c.native_screen_capturer_capture( + source?.nativeHandle ?? 0, + optionsPointer.ref, + callbackCallable.nativeFunction, + NativeCallbacks.userData(callbackCallable), + NativeCallbacks.release, + ); + ScreenCaptureOptions.freeNative(optionsPointer); + } +} diff --git a/bindings/dart/nativeapi/lib/src/screen_recorder.dart b/bindings/dart/nativeapi/lib/src/screen_recorder.dart new file mode 100644 index 00000000..1fdefefd --- /dev/null +++ b/bindings/dart/nativeapi/lib/src/screen_recorder.dart @@ -0,0 +1,300 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +// ignore_for_file: unused_import, unnecessary_import + +import 'dart:async'; +import 'dart:ffi' as ffi; + +import 'package:cnativeapi/cnativeapi.dart' as c; +import 'package:ffi/ffi.dart' as pkg_ffi; + +import 'capture_source.dart'; +import 'screen_capturer.dart'; + +import 'support.dart'; + +import 'callbacks.dart'; + +enum ScreenRecordingState { + idle(0), + starting(1), + recording(2), + stopping(3), + completed(4), + cancelled(5), + failed(6); + + const ScreenRecordingState(this.value); + final int value; + + static ScreenRecordingState fromValue(int value) => switch (value) { + 0 => ScreenRecordingState.idle, + 1 => ScreenRecordingState.starting, + 2 => ScreenRecordingState.recording, + 3 => ScreenRecordingState.stopping, + 4 => ScreenRecordingState.completed, + 5 => ScreenRecordingState.cancelled, + 6 => ScreenRecordingState.failed, + _ => ScreenRecordingState.idle, + }; + + c.native_screen_recording_state_t get raw => + c.native_screen_recording_state_t.fromValue(value); +} + +class ScreenRecordingOptions { + const ScreenRecordingOptions({ + required this.capture, + required this.frameRate, + required this.videoBitrate, + }); + + final ScreenCaptureOptions capture; + final double frameRate; + final int videoBitrate; + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ScreenRecordingOptions && + other.capture == capture && + other.frameRate == frameRate && + other.videoBitrate == videoBitrate); + + @override + int get hashCode => Object.hash(capture, frameRate, videoBitrate); + + @override + String toString() => + 'ScreenRecordingOptions(capture: $capture, frameRate: $frameRate, videoBitrate: $videoBitrate)'; + + factory ScreenRecordingOptions.fromNative( + c.native_screen_recording_options_t raw, + ) => ScreenRecordingOptions( + capture: ScreenCaptureOptions.fromNative(raw.capture), + frameRate: raw.frame_rate, + videoBitrate: raw.video_bitrate, + ); + + /// Allocates the C form; free it with [freeNative]. + ffi.Pointer allocNative() { + final pointer = pkg_ffi.calloc(); + final capturePointer = capture.allocNative(); + pointer.ref.capture = capturePointer.ref; + pkg_ffi.calloc.free(capturePointer); + pointer.ref.frame_rate = frameRate; + pointer.ref.video_bitrate = videoBitrate; + return pointer; + } + + static void freeNative( + ffi.Pointer pointer, + ) { + pkg_ffi.calloc.free(pointer); + } +} + +/// One `ScreenRecordingEvent`, in its concrete form. +sealed class ScreenRecordingEvent { + const ScreenRecordingEvent(); + + /// Reads the event out of its C form. Returns null for a variant this + /// binding does not know about. + static ScreenRecordingEvent? fromNative( + c.native_screen_recording_event_t raw, + ) { + if (raw.typeAsInt == + c + .native_screen_recording_event_type_t + .NATIVE_SCREEN_RECORDING_EVENT_TYPE_STARTED + .value) { + return ScreenRecordingStartedEvent(); + } + if (raw.typeAsInt == + c + .native_screen_recording_event_type_t + .NATIVE_SCREEN_RECORDING_EVENT_TYPE_COMPLETED + .value) { + return ScreenRecordingCompletedEvent( + filePath: raw.data.completed.file_path == ffi.nullptr + ? null + : raw.data.completed.file_path.cast().toDartString(), + duration: raw.data.completed.duration, + ); + } + if (raw.typeAsInt == + c + .native_screen_recording_event_type_t + .NATIVE_SCREEN_RECORDING_EVENT_TYPE_CANCELLED + .value) { + return ScreenRecordingCancelledEvent(); + } + if (raw.typeAsInt == + c + .native_screen_recording_event_type_t + .NATIVE_SCREEN_RECORDING_EVENT_TYPE_FAILED + .value) { + return ScreenRecordingFailedEvent( + message: raw.data.failed.message == ffi.nullptr + ? null + : raw.data.failed.message.cast().toDartString(), + ); + } + return null; + } +} + +final class ScreenRecordingStartedEvent extends ScreenRecordingEvent { + const ScreenRecordingStartedEvent(); +} + +final class ScreenRecordingCompletedEvent extends ScreenRecordingEvent { + const ScreenRecordingCompletedEvent({ + required this.filePath, + required this.duration, + }); + + final String? filePath; + final double duration; +} + +final class ScreenRecordingCancelledEvent extends ScreenRecordingEvent { + const ScreenRecordingCancelledEvent(); +} + +final class ScreenRecordingFailedEvent extends ScreenRecordingEvent { + const ScreenRecordingFailedEvent({required this.message}); + + final String? message; +} + +class ScreenRecorder implements ffi.Finalizable { + /// Adopts a handle returned by the C API and releases it when this + /// object becomes unreachable. + ScreenRecorder.fromHandle(this.nativeHandle) { + _finalizer.attach( + this, + ffi.Pointer.fromAddress(nativeHandle), + detach: this, + ); + } + + /// Wraps a handle owned elsewhere; releasing it stays the owner's job. + ScreenRecorder.borrowed(this.nativeHandle); + + /// The underlying handle-table entry. + final int nativeHandle; + + static final _finalizer = ffi.NativeFinalizer( + ffi.Native.addressOf< + ffi.NativeFunction)> + >(c.native_handle_finalize), + ); + + /// Releases the handle now instead of at collection. + void dispose() { + _finalizer.detach(this); + c.native_screen_recorder_free(nativeHandle); + } + + /// Creates a new `ScreenRecorder`; returns null if the native side failed. + static ScreenRecorder? create() { + final handle = c.native_screen_recorder_create(); + if (handle == 0) return null; + return ScreenRecorder.fromHandle(handle); + } + + static bool isSupported() { + return c.native_screen_recorder_is_supported(); + } + + bool start( + CaptureSource? source, + String filePath, + ScreenRecordingOptions options, + ) { + final filePathNative = filePath.toNativeUtf8().cast(); + final optionsPointer = options.allocNative(); + final result = c.native_screen_recorder_start( + nativeHandle, + source?.nativeHandle ?? 0, + filePathNative, + optionsPointer.ref, + ); + pkg_ffi.calloc.free(filePathNative); + ScreenRecordingOptions.freeNative(optionsPointer); + return result; + } + + bool stop() { + return c.native_screen_recorder_stop(nativeHandle); + } + + bool cancel() { + return c.native_screen_recorder_cancel(nativeHandle); + } + + ScreenRecordingState get state { + final raw = c.native_screen_recorder_get_state(nativeHandle); + return ScreenRecordingState.fromValue(raw.value); + } + + bool get isActive { + return c.native_screen_recorder_is_active(nativeHandle); + } + + double get duration { + return c.native_screen_recorder_get_duration(nativeHandle); + } + + String? get filePath { + final resultPointer = c.native_screen_recorder_get_file_path(nativeHandle); + if (resultPointer == ffi.nullptr) return null; + final result = resultPointer.cast().toDartString(); + c.free_c_str(resultPointer); + return result; + } + + /// Registers [callback] for every `ScreenRecordingEvent` this `ScreenRecorder` emits. + /// + /// Delivered on the registering isolate, including events from native UI threads. + /// The callback may return a Future; borrowed handles stay valid until it completes. + /// Events queued before removal are skipped if their callback has not started. + ListenerId addListener( + FutureOr Function(ScreenRecordingEvent) callback, + ) { + final callable = + ffi.NativeCallable< + ffi.Void Function( + ffi.Pointer, + ffi.Uint64, + ffi.Pointer, + ) + >.listener(( + ffi.Pointer event, + int delivery, + ffi.Pointer _, + ) { + unawaited( + NativeCallbacks.deliverEvent( + delivery, + () => event == ffi.nullptr + ? null + : ScreenRecordingEvent.fromNative(event.ref), + callback, + ), + ); + }); + return c.native_screen_recorder_add_listener_async( + nativeHandle, + callable.nativeFunction, + NativeCallbacks.userData(callable), + NativeCallbacks.release, + ); + } + + /// Unregisters a listener. Returns false if unknown. + bool removeListener(ListenerId listenerId) => + c.native_screen_recorder_remove_listener(nativeHandle, listenerId); +} diff --git a/bindings/dart/nativeapi/lib/src/web/capture_source.dart b/bindings/dart/nativeapi/lib/src/web/capture_source.dart new file mode 100644 index 00000000..8312dc1f --- /dev/null +++ b/bindings/dart/nativeapi/lib/src/web/capture_source.dart @@ -0,0 +1,43 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +// ignore_for_file: unused_import, unnecessary_import + +import 'display.dart'; +import 'window.dart'; +import 'unsupported.dart'; + +enum CaptureSourceType { + none(0), + display(1), + window(2), + unknown(3); + + const CaptureSourceType(this.value); + final int value; + + static CaptureSourceType fromValue(int value) => switch (value) { + 0 => CaptureSourceType.none, + 1 => CaptureSourceType.display, + 2 => CaptureSourceType.window, + 3 => CaptureSourceType.unknown, + _ => CaptureSourceType.none, + }; +} + +class CaptureSource { + CaptureSource.fromHandle(this.nativeHandle); + CaptureSource.borrowed(this.nativeHandle); + + final int nativeHandle; + + void dispose() => unsupported(); + + static CaptureSource? fromDisplay(Display? display) => unsupported(); + + static CaptureSource? fromWindow(Window? window) => unsupported(); + + CaptureSourceType get type => unsupported(); + + String? get name => unsupported(); +} diff --git a/bindings/dart/nativeapi/lib/src/web/generated.dart b/bindings/dart/nativeapi/lib/src/web/generated.dart index ccccad72..c56c2291 100644 --- a/bindings/dart/nativeapi/lib/src/web/generated.dart +++ b/bindings/dart/nativeapi/lib/src/web/generated.dart @@ -4,6 +4,7 @@ export 'accessibility_manager.dart'; export 'app_info.dart'; export 'application.dart'; +export 'capture_source.dart'; export 'clipboard.dart'; export 'device_info.dart'; export 'dialog.dart'; @@ -25,6 +26,8 @@ export 'notification_manager.dart'; export 'placement.dart'; export 'positioning_strategy.dart'; export 'preferences.dart'; +export 'screen_capturer.dart'; +export 'screen_recorder.dart'; export 'secure_storage.dart'; export 'shortcut.dart'; export 'shortcut_manager.dart'; diff --git a/bindings/dart/nativeapi/lib/src/web/screen_capturer.dart b/bindings/dart/nativeapi/lib/src/web/screen_capturer.dart new file mode 100644 index 00000000..98be96ab --- /dev/null +++ b/bindings/dart/nativeapi/lib/src/web/screen_capturer.dart @@ -0,0 +1,87 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +// ignore_for_file: unused_import, unnecessary_import + +import 'capture_source.dart'; +import 'foundation/geometry.dart'; +import 'image.dart'; +import 'window.dart'; +import 'unsupported.dart'; + +class CaptureSourceSelectionOptions { + const CaptureSourceSelectionOptions({ + required this.allowDisplays, + required this.allowWindows, + this.parentWindow, + }); + + final bool allowDisplays; + final bool allowWindows; + final Window? parentWindow; + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is CaptureSourceSelectionOptions && + other.allowDisplays == allowDisplays && + other.allowWindows == allowWindows && + other.parentWindow == parentWindow); + + @override + int get hashCode => Object.hash(allowDisplays, allowWindows, parentWindow); + + @override + String toString() => + 'CaptureSourceSelectionOptions(allowDisplays: $allowDisplays, allowWindows: $allowWindows, parentWindow: $parentWindow)'; +} + +class ScreenCaptureOptions { + const ScreenCaptureOptions({ + required this.includeCursor, + required this.cropEnabled, + required this.crop, + }); + + final bool includeCursor; + final bool cropEnabled; + final Rectangle crop; + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ScreenCaptureOptions && + other.includeCursor == includeCursor && + other.cropEnabled == cropEnabled && + other.crop == crop); + + @override + int get hashCode => Object.hash(includeCursor, cropEnabled, crop); + + @override + String toString() => + 'ScreenCaptureOptions(includeCursor: $includeCursor, cropEnabled: $cropEnabled, crop: $crop)'; +} + +class ScreenCapturer { + const ScreenCapturer._(); + + static const ScreenCapturer instance = ScreenCapturer._(); + + static bool isSupported() => unsupported(); + + static bool isSourceSelectionSupported() => unsupported(); + + static bool isDirectCaptureSupported(CaptureSourceType type) => unsupported(); + + static void selectSource( + CaptureSourceSelectionOptions options, + void Function(bool, CaptureSource?) callback, + ) => unsupported(); + + static void capture( + CaptureSource? source, + ScreenCaptureOptions options, + void Function(bool, Image?) callback, + ) => unsupported(); +} diff --git a/bindings/dart/nativeapi/lib/src/web/screen_recorder.dart b/bindings/dart/nativeapi/lib/src/web/screen_recorder.dart new file mode 100644 index 00000000..f56fafa8 --- /dev/null +++ b/bindings/dart/nativeapi/lib/src/web/screen_recorder.dart @@ -0,0 +1,128 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +// ignore_for_file: unused_import, unnecessary_import + +import 'dart:async'; + +import 'capture_source.dart'; +import 'screen_capturer.dart'; +import 'support.dart'; +import 'unsupported.dart'; + +enum ScreenRecordingState { + idle(0), + starting(1), + recording(2), + stopping(3), + completed(4), + cancelled(5), + failed(6); + + const ScreenRecordingState(this.value); + final int value; + + static ScreenRecordingState fromValue(int value) => switch (value) { + 0 => ScreenRecordingState.idle, + 1 => ScreenRecordingState.starting, + 2 => ScreenRecordingState.recording, + 3 => ScreenRecordingState.stopping, + 4 => ScreenRecordingState.completed, + 5 => ScreenRecordingState.cancelled, + 6 => ScreenRecordingState.failed, + _ => ScreenRecordingState.idle, + }; +} + +class ScreenRecordingOptions { + const ScreenRecordingOptions({ + required this.capture, + required this.frameRate, + required this.videoBitrate, + }); + + final ScreenCaptureOptions capture; + final double frameRate; + final int videoBitrate; + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ScreenRecordingOptions && + other.capture == capture && + other.frameRate == frameRate && + other.videoBitrate == videoBitrate); + + @override + int get hashCode => Object.hash(capture, frameRate, videoBitrate); + + @override + String toString() => + 'ScreenRecordingOptions(capture: $capture, frameRate: $frameRate, videoBitrate: $videoBitrate)'; +} + +/// One `ScreenRecordingEvent`, in its concrete form. +sealed class ScreenRecordingEvent { + const ScreenRecordingEvent(); +} + +final class ScreenRecordingStartedEvent extends ScreenRecordingEvent { + const ScreenRecordingStartedEvent(); +} + +final class ScreenRecordingCompletedEvent extends ScreenRecordingEvent { + const ScreenRecordingCompletedEvent({ + required this.filePath, + required this.duration, + }); + + final String? filePath; + final double duration; +} + +final class ScreenRecordingCancelledEvent extends ScreenRecordingEvent { + const ScreenRecordingCancelledEvent(); +} + +final class ScreenRecordingFailedEvent extends ScreenRecordingEvent { + const ScreenRecordingFailedEvent({required this.message}); + + final String? message; +} + +class ScreenRecorder { + ScreenRecorder.fromHandle(this.nativeHandle); + ScreenRecorder.borrowed(this.nativeHandle); + + final int nativeHandle; + + void dispose() => unsupported(); + + static ScreenRecorder? create() => unsupported(); + + static bool isSupported() => unsupported(); + + bool start( + CaptureSource? source, + String filePath, + ScreenRecordingOptions options, + ) => unsupported(); + + bool stop() => unsupported(); + + bool cancel() => unsupported(); + + ScreenRecordingState get state => unsupported(); + + bool get isActive => unsupported(); + + double get duration => unsupported(); + + String? get filePath => unsupported(); + + ListenerId addListener( + FutureOr Function(ScreenRecordingEvent) callback, + ) => unsupported(); + + bool removeListener(ListenerId listenerId) => unsupported(); +} diff --git a/bindings/go/bridge.gen.h b/bindings/go/bridge.gen.h index adfd1906..8ff47bd3 100644 --- a/bindings/go/bridge.gen.h +++ b/bindings/go/bridge.gen.h @@ -25,6 +25,12 @@ static inline char* go_native_notification_event_t_activated_Argument(const nati extern void goEvent_native_notification_event_t(uintptr_t handle, native_notification_event_t* event); static inline void go_native_notification_event_callback_t(const native_notification_event_t* event, void* data) { goEvent_native_notification_event_t(*(uintptr_t*)data, (native_notification_event_t*)event); } static inline native_notification_event_callback_t go_get_native_notification_event_callback_t(void) { return go_native_notification_event_callback_t; } +static inline char* go_native_screen_recording_event_t_completed_FilePath(const native_screen_recording_event_t* event) { return event->data.completed.file_path; } +static inline double go_native_screen_recording_event_t_completed_Duration(const native_screen_recording_event_t* event) { return event->data.completed.duration; } +static inline char* go_native_screen_recording_event_t_failed_Message(const native_screen_recording_event_t* event) { return event->data.failed.message; } +extern void goEvent_native_screen_recording_event_t(uintptr_t handle, native_screen_recording_event_t* event); +static inline void go_native_screen_recording_event_callback_t(const native_screen_recording_event_t* event, void* data) { goEvent_native_screen_recording_event_t(*(uintptr_t*)data, (native_screen_recording_event_t*)event); } +static inline native_screen_recording_event_callback_t go_get_native_screen_recording_event_callback_t(void) { return go_native_screen_recording_event_callback_t; } static inline char* go_native_view_event_t_text_field_changed_Text(const native_view_event_t* event) { return event->data.text_field_changed.text; } extern void goEvent_native_view_event_t(uintptr_t handle, native_view_event_t* event); static inline void go_native_view_event_callback_t(const native_view_event_t* event, void* data) { goEvent_native_view_event_t(*(uintptr_t*)data, (native_view_event_t*)event); } @@ -87,6 +93,12 @@ static inline native_clipboard_read_image_callback_t go_get_native_clipboard_rea extern void goCall_native_clipboard_read_text_callback_t(uintptr_t handle, bool arg0, void* arg1, native_event_delivery_t delivery); static inline void go_native_clipboard_read_text_callback_t(bool arg0, const char* arg1, native_event_delivery_t delivery, void* data) { goCall_native_clipboard_read_text_callback_t(*(uintptr_t*)data, arg0, (void*)arg1, delivery); } static inline native_clipboard_read_text_callback_t go_get_native_clipboard_read_text_callback_t(void) { return go_native_clipboard_read_text_callback_t; } +extern void goCall_native_screen_capturer_capture_callback_t(uintptr_t handle, bool arg0, native_image_t arg1, native_event_delivery_t delivery); +static inline void go_native_screen_capturer_capture_callback_t(bool arg0, native_image_t arg1, native_event_delivery_t delivery, void* data) { goCall_native_screen_capturer_capture_callback_t(*(uintptr_t*)data, arg0, arg1, delivery); } +static inline native_screen_capturer_capture_callback_t go_get_native_screen_capturer_capture_callback_t(void) { return go_native_screen_capturer_capture_callback_t; } +extern void goCall_native_screen_capturer_select_source_callback_t(uintptr_t handle, bool arg0, native_capture_source_t arg1, native_event_delivery_t delivery); +static inline void go_native_screen_capturer_select_source_callback_t(bool arg0, native_capture_source_t arg1, native_event_delivery_t delivery, void* data) { goCall_native_screen_capturer_select_source_callback_t(*(uintptr_t*)data, arg0, arg1, delivery); } +static inline native_screen_capturer_select_source_callback_t go_get_native_screen_capturer_select_source_callback_t(void) { return go_native_screen_capturer_select_source_callback_t; } extern void goCall_native_shortcut_create_with_id_and_accelerator_and_callback_t(uintptr_t handle); static inline void go_native_shortcut_create_with_id_and_accelerator_and_callback_t(void* data) { goCall_native_shortcut_create_with_id_and_accelerator_and_callback_t(*(uintptr_t*)data); } static inline native_shortcut_create_with_id_and_accelerator_and_callback_t go_get_native_shortcut_create_with_id_and_accelerator_and_callback_t(void) { return go_native_shortcut_create_with_id_and_accelerator_and_callback_t; } diff --git a/bindings/go/nativeapi.gen.go b/bindings/go/nativeapi.gen.go index 560dec8a..ed81c334 100644 --- a/bindings/go/nativeapi.gen.go +++ b/bindings/go/nativeapi.gen.go @@ -2091,6 +2091,388 @@ func (i *Image) NativeObject() unsafe.Pointer { return C.native_image_get_native_object(C.native_image_t(i.nativeHandleValue())) } +// CaptureSourceType represents native CaptureSourceType values. +type CaptureSourceType int32 + +const ( + CaptureSourceTypeNone CaptureSourceType = 0 + CaptureSourceTypeDisplay CaptureSourceType = 1 + CaptureSourceTypeWindow CaptureSourceType = 2 + CaptureSourceTypeUnknown CaptureSourceType = 3 +) + +// CaptureSource owns a native handle. Call Release on the UI thread when done. +// Copies share ownership state; borrowed event handles must not outlive their callback. +type CaptureSource struct{ *nativeHandle } + +func wrapCaptureSource(value uint64, borrowed bool) *CaptureSource { + if value == 0 { + return nil + } + return wrapCaptureSourceState(&nativeHandle{value: value, borrowed: borrowed}) +} +func wrapCaptureSourceState(state *nativeHandle) *CaptureSource { + return &CaptureSource{state} +} +func (cs *CaptureSource) nativeHandleValue() uint64 { + if cs == nil || cs.nativeHandle == nil { + return 0 + } + return cs.nativeHandle.nativeHandleValue() +} + +// Release drops this owned reference. Repeated calls and nil receivers are safe. +func (cs *CaptureSource) Release() { + if cs.nativeHandleValue() == 0 || cs.borrowed { + return + } + C.native_capture_source_free(C.native_capture_source_t(cs.value)) + cs.value = 0 +} + +// CaptureSourceFromDisplay invokes the native FromDisplay operation. +// Failure wraps ErrOperationFailed; the native ABI provides no detailed cause. +func CaptureSourceFromDisplay(display *Display) (*CaptureSource, error) { + result := C.native_capture_source_from_display(C.native_display_t(display.nativeHandleValue())) + if result == 0 { + return nil, nativeError("CaptureSourceFromDisplay") + } + return wrapCaptureSource(uint64(result), false), nil +} + +// CaptureSourceFromWindow invokes the native FromWindow operation. +// Failure wraps ErrOperationFailed; the native ABI provides no detailed cause. +func CaptureSourceFromWindow(window *Window) (*CaptureSource, error) { + result := C.native_capture_source_from_window(C.native_window_t(window.nativeHandleValue())) + if result == 0 { + return nil, nativeError("CaptureSourceFromWindow") + } + return wrapCaptureSource(uint64(result), false), nil +} + +// Type invokes the native GetType operation. +func (cs *CaptureSource) Type() CaptureSourceType { + result := C.native_capture_source_get_type(C.native_capture_source_t(cs.nativeHandleValue())) + return CaptureSourceType(result) +} + +// Name invokes the native GetName operation. +func (cs *CaptureSource) Name() string { + result := C.native_capture_source_get_name(C.native_capture_source_t(cs.nativeHandleValue())) + defer C.free_c_str(result) + return C.GoString(result) +} + +// CaptureSourceSelectionOptions holds the native CaptureSourceSelectionOptions value. +type CaptureSourceSelectionOptions struct { + AllowDisplays bool + AllowWindows bool + ParentWindow *Window +} + +func fromCCaptureSourceSelectionOptions(raw C.native_capture_source_selection_options_t) CaptureSourceSelectionOptions { + return CaptureSourceSelectionOptions{ + AllowDisplays: bool(raw.allow_displays), + AllowWindows: bool(raw.allow_windows), + ParentWindow: wrapWindow(uint64(C.native_handle_retain(raw.parent_window)), false), + } +} +func toCCaptureSourceSelectionOptions(value CaptureSourceSelectionOptions) (C.native_capture_source_selection_options_t, func()) { + var raw C.native_capture_source_selection_options_t + var cleanup []func() + raw.allow_displays = C.bool(value.AllowDisplays) + raw.allow_windows = C.bool(value.AllowWindows) + raw.parent_window = C.native_window_t(value.ParentWindow.nativeHandleValue()) + return raw, func() { + for i := len(cleanup) - 1; i >= 0; i-- { + cleanup[i]() + } + } +} + +// ScreenCaptureOptions holds the native ScreenCaptureOptions value. +type ScreenCaptureOptions struct { + IncludeCursor bool + CropEnabled bool + Crop Rectangle +} + +func fromCScreenCaptureOptions(raw C.native_screen_capture_options_t) ScreenCaptureOptions { + return ScreenCaptureOptions{ + IncludeCursor: bool(raw.include_cursor), + CropEnabled: bool(raw.crop_enabled), + Crop: fromCRectangle(raw.crop), + } +} +func toCScreenCaptureOptions(value ScreenCaptureOptions) (C.native_screen_capture_options_t, func()) { + var raw C.native_screen_capture_options_t + var cleanup []func() + raw.include_cursor = C.bool(value.IncludeCursor) + raw.crop_enabled = C.bool(value.CropEnabled) + cField2, cField2Cleanup := toCRectangle(value.Crop) + cleanup = append(cleanup, func() { cField2Cleanup() }) + raw.crop = cField2 + return raw, func() { + for i := len(cleanup) - 1; i >= 0; i-- { + cleanup[i]() + } + } +} + +// ScreenCapturerIsSupported invokes the native IsSupported operation. +func ScreenCapturerIsSupported() bool { + result := C.native_screen_capturer_is_supported() + return bool(result) +} + +// ScreenCapturerIsSourceSelectionSupported invokes the native IsSourceSelectionSupported operation. +func ScreenCapturerIsSourceSelectionSupported() bool { + result := C.native_screen_capturer_is_source_selection_supported() + return bool(result) +} + +// ScreenCapturerIsDirectCaptureSupported invokes the native IsDirectCaptureSupported operation. +func ScreenCapturerIsDirectCaptureSupported(kind CaptureSourceType) bool { + result := C.native_screen_capturer_is_direct_capture_supported(C.native_capture_source_type_t(kind)) + return bool(result) +} + +// ScreenCapturerSelectSource invokes the native SelectSource operation. +func ScreenCapturerSelectSource(options CaptureSourceSelectionOptions, callback func(bool, *CaptureSource)) { + cArg0, cArg0Cleanup := toCCaptureSourceSelectionOptions(options) + defer cArg0Cleanup() + var cArg1Fn C.native_screen_capturer_select_source_callback_t + var cArg1Data unsafe.Pointer + var cArg1Release C.native_release_user_data_t + if callback != nil { + cArg1Data = newCallback(callback) + cArg1Fn = C.native_screen_capturer_select_source_callback_t(C.go_get_native_screen_capturer_select_source_callback_t()) + cArg1Release = C.native_release_user_data_t(C.go_get_release_callback()) + } + C.native_screen_capturer_select_source(cArg0, cArg1Fn, cArg1Data, cArg1Release) +} + +// ScreenCapturerCapture invokes the native Capture operation. +func ScreenCapturerCapture(source *CaptureSource, options ScreenCaptureOptions, callback func(bool, *Image)) { + cArg1, cArg1Cleanup := toCScreenCaptureOptions(options) + defer cArg1Cleanup() + var cArg2Fn C.native_screen_capturer_capture_callback_t + var cArg2Data unsafe.Pointer + var cArg2Release C.native_release_user_data_t + if callback != nil { + cArg2Data = newCallback(callback) + cArg2Fn = C.native_screen_capturer_capture_callback_t(C.go_get_native_screen_capturer_capture_callback_t()) + cArg2Release = C.native_release_user_data_t(C.go_get_release_callback()) + } + C.native_screen_capturer_capture(C.native_capture_source_t(source.nativeHandleValue()), cArg1, cArg2Fn, cArg2Data, cArg2Release) +} + +// ScreenRecordingState represents native ScreenRecordingState values. +type ScreenRecordingState int32 + +const ( + ScreenRecordingStateIdle ScreenRecordingState = 0 + ScreenRecordingStateStarting ScreenRecordingState = 1 + ScreenRecordingStateRecording ScreenRecordingState = 2 + ScreenRecordingStateStopping ScreenRecordingState = 3 + ScreenRecordingStateCompleted ScreenRecordingState = 4 + ScreenRecordingStateCancelled ScreenRecordingState = 5 + ScreenRecordingStateFailed ScreenRecordingState = 6 +) + +// ScreenRecordingOptions holds the native ScreenRecordingOptions value. +type ScreenRecordingOptions struct { + Capture ScreenCaptureOptions + FrameRate float64 + VideoBitrate uint64 +} + +func fromCScreenRecordingOptions(raw C.native_screen_recording_options_t) ScreenRecordingOptions { + return ScreenRecordingOptions{ + Capture: fromCScreenCaptureOptions(raw.capture), + FrameRate: float64(raw.frame_rate), + VideoBitrate: uint64(raw.video_bitrate), + } +} +func toCScreenRecordingOptions(value ScreenRecordingOptions) (C.native_screen_recording_options_t, func()) { + var raw C.native_screen_recording_options_t + var cleanup []func() + cField0, cField0Cleanup := toCScreenCaptureOptions(value.Capture) + cleanup = append(cleanup, func() { cField0Cleanup() }) + raw.capture = cField0 + raw.frame_rate = C.double(value.FrameRate) + raw.video_bitrate = C.ulong(value.VideoBitrate) + return raw, func() { + for i := len(cleanup) - 1; i >= 0; i-- { + cleanup[i]() + } + } +} + +// ScreenRecordingEventType identifies the active event variant. +type ScreenRecordingEventType int32 + +const ( + ScreenRecordingEventStarted ScreenRecordingEventType = 0 + ScreenRecordingEventCompleted ScreenRecordingEventType = 1 + ScreenRecordingEventCancelled ScreenRecordingEventType = 2 + ScreenRecordingEventFailed ScreenRecordingEventType = 3 +) + +// ScreenRecordingEvent copies an event payload. Handle fields are borrowed until the callback returns. +// Only fields belonging to Type are populated. +type ScreenRecordingEvent struct { + Type ScreenRecordingEventType + FilePath string + Duration float64 + Message string +} + +func fromCScreenRecordingEvent(raw *C.native_screen_recording_event_t) ScreenRecordingEvent { + value := ScreenRecordingEvent{Type: ScreenRecordingEventType(raw._type)} + switch value.Type { + case ScreenRecordingEventStarted: + case ScreenRecordingEventCompleted: + value.FilePath = C.GoString(C.go_native_screen_recording_event_t_completed_FilePath(raw)) + value.Duration = float64(C.go_native_screen_recording_event_t_completed_Duration(raw)) + case ScreenRecordingEventCancelled: + case ScreenRecordingEventFailed: + value.Message = C.GoString(C.go_native_screen_recording_event_t_failed_Message(raw)) + } + return value +} + +//export goEvent_native_screen_recording_event_t +func goEvent_native_screen_recording_event_t(handle C.uintptr_t, event *C.native_screen_recording_event_t) { + cgo.Handle(handle).Value().(func(ScreenRecordingEvent))(fromCScreenRecordingEvent(event)) +} + +// ScreenRecorder owns a native handle. Call Release on the UI thread when done. +// Copies share ownership state; borrowed event handles must not outlive their callback. +type ScreenRecorder struct{ *nativeHandle } + +func wrapScreenRecorder(value uint64, borrowed bool) *ScreenRecorder { + if value == 0 { + return nil + } + return wrapScreenRecorderState(&nativeHandle{value: value, borrowed: borrowed}) +} +func wrapScreenRecorderState(state *nativeHandle) *ScreenRecorder { + return &ScreenRecorder{state} +} +func (sr *ScreenRecorder) nativeHandleValue() uint64 { + if sr == nil || sr.nativeHandle == nil { + return 0 + } + return sr.nativeHandle.nativeHandleValue() +} + +// Release drops this owned reference. Repeated calls and nil receivers are safe. +func (sr *ScreenRecorder) Release() { + if sr.nativeHandleValue() == 0 || sr.borrowed { + return + } + C.native_screen_recorder_free(C.native_screen_recorder_t(sr.value)) + sr.value = 0 +} + +// NewScreenRecorder creates an owned ScreenRecorder. +// Failure wraps ErrOperationFailed; the native ABI provides no detailed cause. +func NewScreenRecorder() (*ScreenRecorder, error) { + result := C.native_screen_recorder_create() + if result == 0 { + return nil, nativeError("NewScreenRecorder") + } + return wrapScreenRecorder(uint64(result), false), nil +} + +// ScreenRecorderIsSupported invokes the native IsSupported operation. +func ScreenRecorderIsSupported() bool { + result := C.native_screen_recorder_is_supported() + return bool(result) +} + +// Start invokes the native Start operation. +// Failure wraps ErrOperationFailed; the native ABI provides no detailed cause. +func (sr *ScreenRecorder) Start(source *CaptureSource, filePath string, options ScreenRecordingOptions) error { + cArg1 := C.CString(filePath) + defer C.free(unsafe.Pointer(cArg1)) + cArg2, cArg2Cleanup := toCScreenRecordingOptions(options) + defer cArg2Cleanup() + result := C.native_screen_recorder_start(C.native_screen_recorder_t(sr.nativeHandleValue()), C.native_capture_source_t(source.nativeHandleValue()), cArg1, cArg2) + if !bool(result) { + return nativeError("ScreenRecorder.Start") + } + return nil +} + +// Stop invokes the native Stop operation. +// Failure wraps ErrOperationFailed; the native ABI provides no detailed cause. +func (sr *ScreenRecorder) Stop() error { + result := C.native_screen_recorder_stop(C.native_screen_recorder_t(sr.nativeHandleValue())) + if !bool(result) { + return nativeError("ScreenRecorder.Stop") + } + return nil +} + +// Cancel invokes the native Cancel operation. +// Failure wraps ErrOperationFailed; the native ABI provides no detailed cause. +func (sr *ScreenRecorder) Cancel() error { + result := C.native_screen_recorder_cancel(C.native_screen_recorder_t(sr.nativeHandleValue())) + if !bool(result) { + return nativeError("ScreenRecorder.Cancel") + } + return nil +} + +// State invokes the native GetState operation. +func (sr *ScreenRecorder) State() ScreenRecordingState { + result := C.native_screen_recorder_get_state(C.native_screen_recorder_t(sr.nativeHandleValue())) + return ScreenRecordingState(result) +} + +// IsActive invokes the native IsActive operation. +func (sr *ScreenRecorder) IsActive() bool { + result := C.native_screen_recorder_is_active(C.native_screen_recorder_t(sr.nativeHandleValue())) + return bool(result) +} + +// Duration invokes the native GetDuration operation. +func (sr *ScreenRecorder) Duration() float64 { + result := C.native_screen_recorder_get_duration(C.native_screen_recorder_t(sr.nativeHandleValue())) + return float64(result) +} + +// FilePath invokes the native GetFilePath operation. +func (sr *ScreenRecorder) FilePath() string { + result := C.native_screen_recorder_get_file_path(C.native_screen_recorder_t(sr.nativeHandleValue())) + defer C.free_c_str(result) + return C.GoString(result) +} + +// AddListener invokes callback synchronously on the native UI thread. +// Remove the listener before releasing its owner. A nil callback is invalid. +func (sr *ScreenRecorder) AddListener(callback func(ScreenRecordingEvent)) (ListenerID, error) { + if callback == nil { + return 0, invalidArgument("ScreenRecorder.AddListener") + } + data := newCallback(callback) + id := C.native_screen_recorder_add_listener(C.native_screen_recorder_t(sr.nativeHandleValue()), C.native_screen_recording_event_callback_t(C.go_get_native_screen_recording_event_callback_t()), data, C.native_release_user_data_t(C.go_get_release_callback())) + if id == 0 { + return 0, nativeError("ScreenRecorder.AddListener") + } + return ListenerID(id), nil +} + +// RemoveListener unregisters id; an unknown id returns an error. +func (sr *ScreenRecorder) RemoveListener(id ListenerID) error { + if !bool(C.native_screen_recorder_remove_listener(C.native_screen_recorder_t(sr.nativeHandleValue()), C.native_listener_id_t(id))) { + return nativeError("ScreenRecorder.RemoveListener") + } + return nil +} + // WindowShape owns a native handle. Call Release on the UI thread when done. // Copies share ownership state; borrowed event handles must not outlive their callback. type WindowShape struct{ *nativeHandle } @@ -6195,6 +6577,24 @@ func goCall_native_clipboard_read_text_callback_t(handle C.uintptr_t, arg0 C.boo cgo.Handle(handle).Value().(func(bool, *string))(bool(arg0), optionalStringFromC((*C.char)(arg1))) } +//export goCall_native_screen_capturer_capture_callback_t +func goCall_native_screen_capturer_capture_callback_t(handle C.uintptr_t, arg0 C.bool, arg1 C.native_image_t, delivery C.native_event_delivery_t) { + defer C.native_event_delivery_complete(delivery, true) + if !bool(C.native_event_delivery_is_active(delivery)) { + return + } + cgo.Handle(handle).Value().(func(bool, *Image))(bool(arg0), wrapImage(uint64(C.native_handle_retain(arg1)), false)) +} + +//export goCall_native_screen_capturer_select_source_callback_t +func goCall_native_screen_capturer_select_source_callback_t(handle C.uintptr_t, arg0 C.bool, arg1 C.native_capture_source_t, delivery C.native_event_delivery_t) { + defer C.native_event_delivery_complete(delivery, true) + if !bool(C.native_event_delivery_is_active(delivery)) { + return + } + cgo.Handle(handle).Value().(func(bool, *CaptureSource))(bool(arg0), wrapCaptureSource(uint64(C.native_handle_retain(arg1)), false)) +} + //export goCall_native_shortcut_create_with_id_and_accelerator_and_callback_t func goCall_native_shortcut_create_with_id_and_accelerator_and_callback_t(handle C.uintptr_t) { cgo.Handle(handle).Value().(func())() diff --git a/bindings/go/screen_capture_test.go b/bindings/go/screen_capture_test.go new file mode 100644 index 00000000..c1f3f091 --- /dev/null +++ b/bindings/go/screen_capture_test.go @@ -0,0 +1,26 @@ +package nativeapi + +import "testing" + +// No desktop or permission: exercise the real cgo nested-value conversion. +func TestRecordingRejectionKeepsIdle(t *testing.T) { + recorder, err := NewScreenRecorder() + if err != nil { + t.Fatal(err) + } + defer recorder.Release() + options := ScreenRecordingOptions{ + Capture: ScreenCaptureOptions{IncludeCursor: false, CropEnabled: true, + Crop: Rectangle{X: 1, Y: 2, Width: 32, Height: 48}}, + FrameRate: 30, VideoBitrate: 1000000, + } + if recorder.Start(nil, "relative.mp4", options) == nil { + t.Fatal("invalid request accepted") + } + if recorder.State() != ScreenRecordingStateIdle || recorder.IsActive() || recorder.Duration() != 0 { + t.Fatal("rejected request changed recorder state") + } + if recorder.Stop() == nil || recorder.Cancel() == nil { + t.Fatal("idle recorder accepted a termination action") + } +} diff --git a/bindings/js/lib/capture_source.ts b/bindings/js/lib/capture_source.ts new file mode 100644 index 00000000..7539b25d --- /dev/null +++ b/bindings/js/lib/capture_source.ts @@ -0,0 +1,38 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +import { native, NativeObject, wrapHandle } from "./runtime.ts"; +import { Display } from "./display.ts"; +import { Window } from "./window.ts"; + +export const CaptureSourceType = { + None: 0, + Display: 1, + Window: 2, + Unknown: 3, +} as const; +export type CaptureSourceType = (typeof CaptureSourceType)[keyof typeof CaptureSourceType]; + +/** A native CaptureSource, held through an owned handle. */ +export class CaptureSource extends NativeObject { + /** Wraps a raw handle; an owned one is released on `dispose()` or collection. */ + constructor(handle: bigint, owned = true) { + super(handle, owned ? native.native_capture_source_free : undefined); + } + + static fromDisplay(display: Display | null): CaptureSource | null { + return wrapHandle(CaptureSource, native.native_capture_source_from_display(display?.nativeHandle ?? 0n)); + } + + static fromWindow(window: Window | null): CaptureSource | null { + return wrapHandle(CaptureSource, native.native_capture_source_from_window(window?.nativeHandle ?? 0n)); + } + + get type(): CaptureSourceType { + return native.native_capture_source_get_type(this.nativeHandle); + } + + get name(): string { + return native.native_capture_source_get_name(this.nativeHandle); + } +} diff --git a/bindings/js/lib/modules.ts b/bindings/js/lib/modules.ts index c7669b4c..53d9dea3 100644 --- a/bindings/js/lib/modules.ts +++ b/bindings/js/lib/modules.ts @@ -21,6 +21,9 @@ export * from "./message_dialog.ts"; export * from "./file_dialog.ts"; export * from "./notification_manager.ts"; export * from "./image.ts"; +export * from "./capture_source.ts"; +export * from "./screen_capturer.ts"; +export * from "./screen_recorder.ts"; export * from "./window_shape.ts"; export * from "./window_shadow.ts"; export * from "./view.ts"; diff --git a/bindings/js/lib/screen_capturer.ts b/bindings/js/lib/screen_capturer.ts new file mode 100644 index 00000000..f16d3900 --- /dev/null +++ b/bindings/js/lib/screen_capturer.ts @@ -0,0 +1,44 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +import { native, deliverEvent } from "./runtime.ts"; +import { CaptureSource, CaptureSourceType } from "./capture_source.ts"; +import { type Rectangle } from "./geometry.ts"; +import { Image } from "./image.ts"; +import { Window } from "./window.ts"; + +export interface CaptureSourceSelectionOptions { + allowDisplays: boolean; + allowWindows: boolean; + parentWindow?: Window | null; +} + +export interface ScreenCaptureOptions { + includeCursor: boolean; + cropEnabled: boolean; + crop: Rectangle; +} + +export class ScreenCapturer { + private constructor() {} + + static isSupported(): boolean { + return native.native_screen_capturer_is_supported(); + } + + static isSourceSelectionSupported(): boolean { + return native.native_screen_capturer_is_source_selection_supported(); + } + + static isDirectCaptureSupported(type: CaptureSourceType): boolean { + return native.native_screen_capturer_is_direct_capture_supported(type); + } + + static selectSource(options: CaptureSourceSelectionOptions, callback: (arg0: boolean, arg1: CaptureSource | null) => void): void { + native.native_screen_capturer_select_source(({ ...options, parentWindow: options.parentWindow?.nativeHandle ?? 0n }), (arg0: any, arg1: any, delivery: bigint) => deliverEvent(delivery, () => [arg0, (arg1 ? new CaptureSource(native.retainHandle(arg1)) : null)] as const, (values) => callback(...values))); + } + + static capture(source: CaptureSource | null, options: ScreenCaptureOptions, callback: (arg0: boolean, arg1: Image | null) => void): void { + native.native_screen_capturer_capture(source?.nativeHandle ?? 0n, ({ ...options, crop: options.crop }), (arg0: any, arg1: any, delivery: bigint) => deliverEvent(delivery, () => [arg0, (arg1 ? new Image(native.retainHandle(arg1)) : null)] as const, (values) => callback(...values))); + } +} diff --git a/bindings/js/lib/screen_recorder.ts b/bindings/js/lib/screen_recorder.ts new file mode 100644 index 00000000..f517e1f5 --- /dev/null +++ b/bindings/js/lib/screen_recorder.ts @@ -0,0 +1,87 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +import { native, NativeObject, deliverEvent } from "./runtime.ts"; +import { CaptureSource } from "./capture_source.ts"; +import { type ScreenCaptureOptions } from "./screen_capturer.ts"; + +export const ScreenRecordingState = { + Idle: 0, + Starting: 1, + Recording: 2, + Stopping: 3, + Completed: 4, + Cancelled: 5, + Failed: 6, +} as const; +export type ScreenRecordingState = (typeof ScreenRecordingState)[keyof typeof ScreenRecordingState]; + +export interface ScreenRecordingOptions { + capture: ScreenCaptureOptions; + frameRate: number; + videoBitrate: number; +} + +export type ScreenRecordingEvent = + | { type: "started" } + | { type: "completed"; filePath: string | null; duration: number } + | { type: "cancelled" } + | { type: "failed"; message: string | null }; + +/** A native ScreenRecorder, held through an owned handle. */ +export class ScreenRecorder extends NativeObject { + /** Wraps a raw handle; an owned one is released on `dispose()` or collection. */ + constructor(handle: bigint, owned = true) { + super(handle, owned ? native.native_screen_recorder_free : undefined); + } + + static create(): ScreenRecorder | null { + const handle: bigint = native.native_screen_recorder_create(); + return handle ? new ScreenRecorder(handle) : null; + } + + static isSupported(): boolean { + return native.native_screen_recorder_is_supported(); + } + + start(source: CaptureSource | null, filePath: string, options: ScreenRecordingOptions): boolean { + return native.native_screen_recorder_start(this.nativeHandle, source?.nativeHandle ?? 0n, filePath, ({ ...options, capture: ({ ...options.capture, crop: options.capture.crop }) })); + } + + stop(): boolean { + return native.native_screen_recorder_stop(this.nativeHandle); + } + + cancel(): boolean { + return native.native_screen_recorder_cancel(this.nativeHandle); + } + + get state(): ScreenRecordingState { + return native.native_screen_recorder_get_state(this.nativeHandle); + } + + get isActive(): boolean { + return native.native_screen_recorder_is_active(this.nativeHandle); + } + + get duration(): number { + return native.native_screen_recorder_get_duration(this.nativeHandle); + } + + get filePath(): string { + return native.native_screen_recorder_get_file_path(this.nativeHandle); + } + + /** Receives events on the JS thread. Borrowed objects stay valid until the returned Promise settles. */ + addListener(listener: (event: ScreenRecordingEvent) => void | Promise): number { + return native.native_screen_recorder_add_listener(this.nativeHandle, (event: Record, delivery: bigint) => + deliverEvent(delivery, () => { + return event as unknown as ScreenRecordingEvent; + }, listener)); + } + + /** Unregisters a listener; returns false if the id is unknown. */ + removeListener(listenerId: number): boolean { + return native.native_screen_recorder_remove_listener(this.nativeHandle, listenerId); + } +} diff --git a/bindings/js/src/generated/capture_source.cc b/bindings/js/src/generated/capture_source.cc new file mode 100644 index 00000000..77c4cb85 --- /dev/null +++ b/bindings/js/src/generated/capture_source.cc @@ -0,0 +1,96 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "types.h" + +namespace nativeapi_js { +namespace { + +napi_value Js_native_capture_source_free(napi_env env, napi_callback_info info) { + Args args(env, info); + if (!args.ok()) { + return nullptr; + } + Arena arena; + (void)arena; + uint64_t self = 0; + if (!GetHandle(env, args[0], &self)) { + return nullptr; + } + OnMainThread([&] { return native_capture_source_free(self); }); + return Undefined(env); +} + +napi_value Js_native_capture_source_from_display(napi_env env, napi_callback_info info) { + Args args(env, info); + if (!args.ok()) { + return nullptr; + } + Arena arena; + (void)arena; + native_display_t p0 = {}; + if (!GetHandle(env, args[0], &p0)) { + return nullptr; + } + auto result = OnMainThread([&] { return native_capture_source_from_display(p0); }); + return Value::BigInt(result).ToJs(env); +} + +napi_value Js_native_capture_source_from_window(napi_env env, napi_callback_info info) { + Args args(env, info); + if (!args.ok()) { + return nullptr; + } + Arena arena; + (void)arena; + native_window_t p0 = {}; + if (!GetHandle(env, args[0], &p0)) { + return nullptr; + } + auto result = OnMainThread([&] { return native_capture_source_from_window(p0); }); + return Value::BigInt(result).ToJs(env); +} + +napi_value Js_native_capture_source_get_type(napi_env env, napi_callback_info info) { + Args args(env, info); + if (!args.ok()) { + return nullptr; + } + Arena arena; + (void)arena; + uint64_t self = 0; + if (!GetHandle(env, args[0], &self)) { + return nullptr; + } + auto result = OnMainThread([&] { return native_capture_source_get_type(self); }); + return Value::Number(static_cast(result)).ToJs(env); +} + +napi_value Js_native_capture_source_get_name(napi_env env, napi_callback_info info) { + Args args(env, info); + if (!args.ok()) { + return nullptr; + } + Arena arena; + (void)arena; + uint64_t self = 0; + if (!GetHandle(env, args[0], &self)) { + return nullptr; + } + auto result = OnMainThread([&] { return native_capture_source_get_name(self); }); + Value value = Value::String(result ? result : ""); + free_c_str(result); + return value.ToJs(env); +} + +} // namespace + +void RegisterCaptureSource(napi_env env, napi_value exports) { + Export(env, exports, "native_capture_source_free", Js_native_capture_source_free); + Export(env, exports, "native_capture_source_from_display", Js_native_capture_source_from_display); + Export(env, exports, "native_capture_source_from_window", Js_native_capture_source_from_window); + Export(env, exports, "native_capture_source_get_type", Js_native_capture_source_get_type); + Export(env, exports, "native_capture_source_get_name", Js_native_capture_source_get_name); +} + +} // namespace nativeapi_js diff --git a/bindings/js/src/generated/modules.cc b/bindings/js/src/generated/modules.cc index 3d4ba3b7..34c56a8a 100644 --- a/bindings/js/src/generated/modules.cc +++ b/bindings/js/src/generated/modules.cc @@ -25,6 +25,9 @@ void RegisterMessageDialog(napi_env env, napi_value exports); void RegisterFileDialog(napi_env env, napi_value exports); void RegisterNotificationManager(napi_env env, napi_value exports); void RegisterImage(napi_env env, napi_value exports); +void RegisterCaptureSource(napi_env env, napi_value exports); +void RegisterScreenCapturer(napi_env env, napi_value exports); +void RegisterScreenRecorder(napi_env env, napi_value exports); void RegisterWindowShape(napi_env env, napi_value exports); void RegisterWindowShadow(napi_env env, napi_value exports); void RegisterView(napi_env env, napi_value exports); @@ -63,6 +66,9 @@ void RegisterGenerated(napi_env env, napi_value exports) { RegisterFileDialog(env, exports); RegisterNotificationManager(env, exports); RegisterImage(env, exports); + RegisterCaptureSource(env, exports); + RegisterScreenCapturer(env, exports); + RegisterScreenRecorder(env, exports); RegisterWindowShape(env, exports); RegisterWindowShadow(env, exports); RegisterView(env, exports); diff --git a/bindings/js/src/generated/screen_capturer.cc b/bindings/js/src/generated/screen_capturer.cc new file mode 100644 index 00000000..30c23d09 --- /dev/null +++ b/bindings/js/src/generated/screen_capturer.cc @@ -0,0 +1,98 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "types.h" + +namespace nativeapi_js { +namespace { + +napi_value Js_native_screen_capturer_is_supported(napi_env env, napi_callback_info info) { + Args args(env, info); + if (!args.ok()) { + return nullptr; + } + Arena arena; + (void)arena; + auto result = OnMainThread([&] { return native_screen_capturer_is_supported(); }); + return Value::Bool(result).ToJs(env); +} + +napi_value Js_native_screen_capturer_is_source_selection_supported(napi_env env, napi_callback_info info) { + Args args(env, info); + if (!args.ok()) { + return nullptr; + } + Arena arena; + (void)arena; + auto result = OnMainThread([&] { return native_screen_capturer_is_source_selection_supported(); }); + return Value::Bool(result).ToJs(env); +} + +napi_value Js_native_screen_capturer_is_direct_capture_supported(napi_env env, napi_callback_info info) { + Args args(env, info); + if (!args.ok()) { + return nullptr; + } + Arena arena; + (void)arena; + native_capture_source_type_t p0 = {}; + if (!GetNumber(env, args[0], &p0)) { + return nullptr; + } + auto result = OnMainThread([&] { return native_screen_capturer_is_direct_capture_supported(p0); }); + return Value::Bool(result).ToJs(env); +} + +napi_value Js_native_screen_capturer_select_source(napi_env env, napi_callback_info info) { + Args args(env, info); + if (!args.ok()) { + return nullptr; + } + Arena arena; + (void)arena; + native_capture_source_selection_options_t p0 = {}; + if (!FromJs(env, args[0], &p0, arena)) { + return nullptr; + } + Callback* p1 = nullptr; + if (!GetCallback(env, args[1], /*optional=*/false, &p1)) { + return nullptr; + } + OnMainThread([&] { return native_screen_capturer_select_source(p0, +[](bool arg0, native_capture_source_t arg1, native_event_delivery_t delivery, void* user_data) { Callback::DispatchEvent(user_data, {Value::Bool(arg0), Value::BigInt(arg1)}, delivery); }, p1, &Callback::ReleaseUserData); }); + return Undefined(env); +} + +napi_value Js_native_screen_capturer_capture(napi_env env, napi_callback_info info) { + Args args(env, info); + if (!args.ok()) { + return nullptr; + } + Arena arena; + (void)arena; + native_capture_source_t p0 = {}; + if (!GetHandle(env, args[0], &p0)) { + return nullptr; + } + native_screen_capture_options_t p1 = {}; + if (!FromJs(env, args[1], &p1, arena)) { + return nullptr; + } + Callback* p2 = nullptr; + if (!GetCallback(env, args[2], /*optional=*/false, &p2)) { + return nullptr; + } + OnMainThread([&] { return native_screen_capturer_capture(p0, p1, +[](bool arg0, native_image_t arg1, native_event_delivery_t delivery, void* user_data) { Callback::DispatchEvent(user_data, {Value::Bool(arg0), Value::BigInt(arg1)}, delivery); }, p2, &Callback::ReleaseUserData); }); + return Undefined(env); +} + +} // namespace + +void RegisterScreenCapturer(napi_env env, napi_value exports) { + Export(env, exports, "native_screen_capturer_is_supported", Js_native_screen_capturer_is_supported); + Export(env, exports, "native_screen_capturer_is_source_selection_supported", Js_native_screen_capturer_is_source_selection_supported); + Export(env, exports, "native_screen_capturer_is_direct_capture_supported", Js_native_screen_capturer_is_direct_capture_supported); + Export(env, exports, "native_screen_capturer_select_source", Js_native_screen_capturer_select_source); + Export(env, exports, "native_screen_capturer_capture", Js_native_screen_capturer_capture); +} + +} // namespace nativeapi_js diff --git a/bindings/js/src/generated/screen_recorder.cc b/bindings/js/src/generated/screen_recorder.cc new file mode 100644 index 00000000..beb0626c --- /dev/null +++ b/bindings/js/src/generated/screen_recorder.cc @@ -0,0 +1,221 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. + +#include "types.h" + +namespace nativeapi_js { +namespace { + +napi_value Js_native_screen_recorder_create(napi_env env, napi_callback_info info) { + Args args(env, info); + if (!args.ok()) { + return nullptr; + } + Arena arena; + (void)arena; + auto result = OnMainThread([&] { return native_screen_recorder_create(); }); + return Value::BigInt(result).ToJs(env); +} + +napi_value Js_native_screen_recorder_free(napi_env env, napi_callback_info info) { + Args args(env, info); + if (!args.ok()) { + return nullptr; + } + Arena arena; + (void)arena; + uint64_t self = 0; + if (!GetHandle(env, args[0], &self)) { + return nullptr; + } + OnMainThread([&] { return native_screen_recorder_free(self); }); + return Undefined(env); +} + +napi_value Js_native_screen_recorder_is_supported(napi_env env, napi_callback_info info) { + Args args(env, info); + if (!args.ok()) { + return nullptr; + } + Arena arena; + (void)arena; + auto result = OnMainThread([&] { return native_screen_recorder_is_supported(); }); + return Value::Bool(result).ToJs(env); +} + +napi_value Js_native_screen_recorder_start(napi_env env, napi_callback_info info) { + Args args(env, info); + if (!args.ok()) { + return nullptr; + } + Arena arena; + (void)arena; + uint64_t self = 0; + if (!GetHandle(env, args[0], &self)) { + return nullptr; + } + native_capture_source_t p0 = {}; + if (!GetHandle(env, args[1], &p0)) { + return nullptr; + } + const char* p1 = {}; + if (!GetString(env, args[2], arena, &p1)) { + return nullptr; + } + native_screen_recording_options_t p2 = {}; + if (!FromJs(env, args[3], &p2, arena)) { + return nullptr; + } + auto result = OnMainThread([&] { return native_screen_recorder_start(self, p0, p1, p2); }); + return Value::Bool(result).ToJs(env); +} + +napi_value Js_native_screen_recorder_stop(napi_env env, napi_callback_info info) { + Args args(env, info); + if (!args.ok()) { + return nullptr; + } + Arena arena; + (void)arena; + uint64_t self = 0; + if (!GetHandle(env, args[0], &self)) { + return nullptr; + } + auto result = OnMainThread([&] { return native_screen_recorder_stop(self); }); + return Value::Bool(result).ToJs(env); +} + +napi_value Js_native_screen_recorder_cancel(napi_env env, napi_callback_info info) { + Args args(env, info); + if (!args.ok()) { + return nullptr; + } + Arena arena; + (void)arena; + uint64_t self = 0; + if (!GetHandle(env, args[0], &self)) { + return nullptr; + } + auto result = OnMainThread([&] { return native_screen_recorder_cancel(self); }); + return Value::Bool(result).ToJs(env); +} + +napi_value Js_native_screen_recorder_get_state(napi_env env, napi_callback_info info) { + Args args(env, info); + if (!args.ok()) { + return nullptr; + } + Arena arena; + (void)arena; + uint64_t self = 0; + if (!GetHandle(env, args[0], &self)) { + return nullptr; + } + auto result = OnMainThread([&] { return native_screen_recorder_get_state(self); }); + return Value::Number(static_cast(result)).ToJs(env); +} + +napi_value Js_native_screen_recorder_is_active(napi_env env, napi_callback_info info) { + Args args(env, info); + if (!args.ok()) { + return nullptr; + } + Arena arena; + (void)arena; + uint64_t self = 0; + if (!GetHandle(env, args[0], &self)) { + return nullptr; + } + auto result = OnMainThread([&] { return native_screen_recorder_is_active(self); }); + return Value::Bool(result).ToJs(env); +} + +napi_value Js_native_screen_recorder_get_duration(napi_env env, napi_callback_info info) { + Args args(env, info); + if (!args.ok()) { + return nullptr; + } + Arena arena; + (void)arena; + uint64_t self = 0; + if (!GetHandle(env, args[0], &self)) { + return nullptr; + } + auto result = OnMainThread([&] { return native_screen_recorder_get_duration(self); }); + return Value::Number(static_cast(result)).ToJs(env); +} + +napi_value Js_native_screen_recorder_get_file_path(napi_env env, napi_callback_info info) { + Args args(env, info); + if (!args.ok()) { + return nullptr; + } + Arena arena; + (void)arena; + uint64_t self = 0; + if (!GetHandle(env, args[0], &self)) { + return nullptr; + } + auto result = OnMainThread([&] { return native_screen_recorder_get_file_path(self); }); + Value value = Value::String(result ? result : ""); + free_c_str(result); + return value.ToJs(env); +} + +napi_value Js_native_screen_recorder_add_listener(napi_env env, napi_callback_info info) { + Args args(env, info); + if (!args.ok()) { + return nullptr; + } + uint64_t self = 0; + if (!GetHandle(env, args[0], &self)) { + return nullptr; + } + Callback* callback = nullptr; + if (!GetCallback(env, args[1], /*optional=*/false, &callback)) { + return nullptr; + } + native_listener_id_t id = OnMainThread([&] { return native_screen_recorder_add_listener_async(self, +[](const native_screen_recording_event_t* event, native_event_delivery_t delivery, void* user_data) { + if (event != nullptr) { + Callback::DispatchEvent(user_data, {ToValue(*event)}, delivery); + } else { native_event_delivery_complete(delivery, false); } + }, callback, &Callback::ReleaseUserData); }); + if (id) Callback::AttachRegistration(callback, [self, id] { (void)native_screen_recorder_remove_listener(self, id); }); + return Value::Number(static_cast(id)).ToJs(env); +} + +napi_value Js_native_screen_recorder_remove_listener(napi_env env, napi_callback_info info) { + Args args(env, info); + if (!args.ok()) { + return nullptr; + } + uint64_t self = 0; + if (!GetHandle(env, args[0], &self)) { + return nullptr; + } + native_listener_id_t id = 0; + if (!GetNumber(env, args[1], &id)) { + return nullptr; + } + bool removed = OnMainThread([&] { return native_screen_recorder_remove_listener(self, id); }); + return Value::Bool(removed).ToJs(env); +} + +} // namespace + +void RegisterScreenRecorder(napi_env env, napi_value exports) { + Export(env, exports, "native_screen_recorder_create", Js_native_screen_recorder_create); + Export(env, exports, "native_screen_recorder_free", Js_native_screen_recorder_free); + Export(env, exports, "native_screen_recorder_is_supported", Js_native_screen_recorder_is_supported); + Export(env, exports, "native_screen_recorder_start", Js_native_screen_recorder_start); + Export(env, exports, "native_screen_recorder_stop", Js_native_screen_recorder_stop); + Export(env, exports, "native_screen_recorder_cancel", Js_native_screen_recorder_cancel); + Export(env, exports, "native_screen_recorder_get_state", Js_native_screen_recorder_get_state); + Export(env, exports, "native_screen_recorder_is_active", Js_native_screen_recorder_is_active); + Export(env, exports, "native_screen_recorder_get_duration", Js_native_screen_recorder_get_duration); + Export(env, exports, "native_screen_recorder_get_file_path", Js_native_screen_recorder_get_file_path); + Export(env, exports, "native_screen_recorder_add_listener", Js_native_screen_recorder_add_listener); + Export(env, exports, "native_screen_recorder_remove_listener", Js_native_screen_recorder_remove_listener); +} + +} // namespace nativeapi_js diff --git a/bindings/js/src/generated/types.h b/bindings/js/src/generated/types.h index 7c10a62a..9c93e8ca 100644 --- a/bindings/js/src/generated/types.h +++ b/bindings/js/src/generated/types.h @@ -25,6 +25,9 @@ #include "capi/file_dialog_c.h" #include "capi/notification_manager_c.h" #include "capi/image_c.h" +#include "capi/capture_source_c.h" +#include "capi/screen_capturer_c.h" +#include "capi/screen_recorder_c.h" #include "capi/window_shape_c.h" #include "capi/window_shadow_c.h" #include "capi/view_c.h" @@ -64,6 +67,13 @@ inline Value ToValue(const native_display_event_t& event); inline Value ToValue(const native_url_open_result_t& value); inline bool FromJs(napi_env env, napi_value value, native_url_open_result_t* out, Arena& arena); inline Value ToValue(const native_notification_event_t& event); +inline Value ToValue(const native_capture_source_selection_options_t& value); +inline bool FromJs(napi_env env, napi_value value, native_capture_source_selection_options_t* out, Arena& arena); +inline Value ToValue(const native_screen_capture_options_t& value); +inline bool FromJs(napi_env env, napi_value value, native_screen_capture_options_t* out, Arena& arena); +inline Value ToValue(const native_screen_recording_options_t& value); +inline bool FromJs(napi_env env, napi_value value, native_screen_recording_options_t* out, Arena& arena); +inline Value ToValue(const native_screen_recording_event_t& event); inline Value ToValue(const native_view_event_t& event); inline Value ToValue(const native_window_event_t& event); inline Value ToValue(const native_window_drag_event_t& event); @@ -469,6 +479,150 @@ inline Value ToValue(const native_notification_event_t& event) { return result; } +inline Value ToValue(const native_capture_source_selection_options_t& value) { + Value result = Value::Object(); + result.Set("allowDisplays", Value::Bool(value.allow_displays)); + result.Set("allowWindows", Value::Bool(value.allow_windows)); + result.Set("parentWindow", Value::BigInt(value.parent_window)); + return result; +} + +inline bool FromJs(napi_env env, napi_value value, native_capture_source_selection_options_t* out, Arena& arena) { + if (!ExpectObject(env, value, "CaptureSourceSelectionOptions")) { + return false; + } + napi_value field = nullptr; + if (!GetField(env, value, "allowDisplays", &field)) { + return false; + } + if (field != nullptr) { + if (!GetBool(env, field, &out->allow_displays)) { + return false; + } + } + if (!GetField(env, value, "allowWindows", &field)) { + return false; + } + if (field != nullptr) { + if (!GetBool(env, field, &out->allow_windows)) { + return false; + } + } + if (!GetField(env, value, "parentWindow", &field)) { + return false; + } + if (field != nullptr) { + if (!GetHandle(env, field, &out->parent_window)) { + return false; + } + } + return true; +} + +inline Value ToValue(const native_screen_capture_options_t& value) { + Value result = Value::Object(); + result.Set("includeCursor", Value::Bool(value.include_cursor)); + result.Set("cropEnabled", Value::Bool(value.crop_enabled)); + result.Set("crop", ToValue(value.crop)); + return result; +} + +inline bool FromJs(napi_env env, napi_value value, native_screen_capture_options_t* out, Arena& arena) { + if (!ExpectObject(env, value, "ScreenCaptureOptions")) { + return false; + } + napi_value field = nullptr; + if (!GetField(env, value, "includeCursor", &field)) { + return false; + } + if (field != nullptr) { + if (!GetBool(env, field, &out->include_cursor)) { + return false; + } + } + if (!GetField(env, value, "cropEnabled", &field)) { + return false; + } + if (field != nullptr) { + if (!GetBool(env, field, &out->crop_enabled)) { + return false; + } + } + if (!GetField(env, value, "crop", &field)) { + return false; + } + if (field != nullptr) { + if (!FromJs(env, field, &out->crop, arena)) { + return false; + } + } + return true; +} + +inline Value ToValue(const native_screen_recording_options_t& value) { + Value result = Value::Object(); + result.Set("capture", ToValue(value.capture)); + result.Set("frameRate", Value::Number(static_cast(value.frame_rate))); + result.Set("videoBitrate", Value::Number(static_cast(value.video_bitrate))); + return result; +} + +inline bool FromJs(napi_env env, napi_value value, native_screen_recording_options_t* out, Arena& arena) { + if (!ExpectObject(env, value, "ScreenRecordingOptions")) { + return false; + } + napi_value field = nullptr; + if (!GetField(env, value, "capture", &field)) { + return false; + } + if (field != nullptr) { + if (!FromJs(env, field, &out->capture, arena)) { + return false; + } + } + if (!GetField(env, value, "frameRate", &field)) { + return false; + } + if (field != nullptr) { + if (!GetNumber(env, field, &out->frame_rate)) { + return false; + } + } + if (!GetField(env, value, "videoBitrate", &field)) { + return false; + } + if (field != nullptr) { + if (!GetNumber(env, field, &out->video_bitrate)) { + return false; + } + } + return true; +} + +inline Value ToValue(const native_screen_recording_event_t& event) { + Value result = Value::Object(); + switch (event.type) { + case NATIVE_SCREEN_RECORDING_EVENT_TYPE_STARTED: + result.Set("type", Value::String("started")); + break; + case NATIVE_SCREEN_RECORDING_EVENT_TYPE_COMPLETED: + result.Set("type", Value::String("completed")); + result.Set("filePath", Value::String(event.data.completed.file_path)); + result.Set("duration", Value::Number(static_cast(event.data.completed.duration))); + break; + case NATIVE_SCREEN_RECORDING_EVENT_TYPE_CANCELLED: + result.Set("type", Value::String("cancelled")); + break; + case NATIVE_SCREEN_RECORDING_EVENT_TYPE_FAILED: + result.Set("type", Value::String("failed")); + result.Set("message", Value::String(event.data.failed.message)); + break; + default: + return Value::Null(); + } + return result; +} + inline Value ToValue(const native_view_event_t& event) { Value result = Value::Object(); switch (event.type) { diff --git a/bindings/js/test/screen_capture.test.ts b/bindings/js/test/screen_capture.test.ts new file mode 100644 index 00000000..8b5ccf79 --- /dev/null +++ b/bindings/js/test/screen_capture.test.ts @@ -0,0 +1,28 @@ +// No UI or permission request: real addon conversions and rejected lifecycle. +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { CaptureSourceType, ScreenCapturer, ScreenRecorder, ScreenRecordingState } from "../lib/index.ts"; + +test("static capture entry rejects invalid direct types", () => { + assert.equal(ScreenCapturer.isDirectCaptureSupported(CaptureSourceType.None), false); + assert.equal(ScreenCapturer.isDirectCaptureSupported(CaptureSourceType.Unknown), false); +}); + +test("nested recording options cross the ABI without creating an output", () => { + const recorder = ScreenRecorder.create()!; + try { + assert.equal(recorder.state, ScreenRecordingState.Idle); + assert.equal(recorder.start(null, "relative.mp4", { + capture: { includeCursor: false, cropEnabled: true, crop: { x: 1, y: 2, width: 32, height: 48 } }, + frameRate: 30, + videoBitrate: 1_000_000, + }), false); + assert.equal(recorder.state, ScreenRecordingState.Idle); + assert.equal(recorder.isActive, false); + assert.equal(recorder.duration, 0); + assert.equal(recorder.stop(), false); + assert.equal(recorder.cancel(), false); + } finally { + recorder.dispose(); + } +}); diff --git a/bindings/python/nativeapi/__init__.py b/bindings/python/nativeapi/__init__.py index 2d26d5a6..2e19a7bc 100644 --- a/bindings/python/nativeapi/__init__.py +++ b/bindings/python/nativeapi/__init__.py @@ -24,6 +24,10 @@ ApplicationStartedEvent, Brightness, ) +from .capture_source import ( + CaptureSource, + CaptureSourceType, +) from .clipboard import ( Clipboard, ClipboardChangedEvent, @@ -131,6 +135,21 @@ from .preferences import ( Preferences, ) +from .screen_capturer import ( + CaptureSourceSelectionOptions, + ScreenCaptureOptions, + ScreenCapturer, +) +from .screen_recorder import ( + ScreenRecorder, + ScreenRecordingCancelledEvent, + ScreenRecordingCompletedEvent, + ScreenRecordingEvent, + ScreenRecordingFailedEvent, + ScreenRecordingOptions, + ScreenRecordingStartedEvent, + ScreenRecordingState, +) from .secure_storage import ( SecureStorage, ) @@ -240,6 +259,9 @@ "Brightness", "Button", "ButtonClickedEvent", + "CaptureSource", + "CaptureSourceSelectionOptions", + "CaptureSourceType", "Clipboard", "ClipboardChangedEvent", "ClipboardData", @@ -310,6 +332,16 @@ "Preferences", "Rectangle", "ResizeEdge", + "ScreenCaptureOptions", + "ScreenCapturer", + "ScreenRecorder", + "ScreenRecordingCancelledEvent", + "ScreenRecordingCompletedEvent", + "ScreenRecordingEvent", + "ScreenRecordingFailedEvent", + "ScreenRecordingOptions", + "ScreenRecordingStartedEvent", + "ScreenRecordingState", "SecureStorage", "Shortcut", "ShortcutActivatedEvent", diff --git a/bindings/python/nativeapi/_capi.py b/bindings/python/nativeapi/_capi.py index 4f808fc7..46bd29b6 100644 --- a/bindings/python/nativeapi/_capi.py +++ b/bindings/python/nativeapi/_capi.py @@ -121,6 +121,34 @@ class native_notification_event_t(Structure): pass +class native_capture_source_selection_options_t(Structure): + pass + + +class native_screen_capture_options_t(Structure): + pass + + +class native_screen_recording_options_t(Structure): + pass + + +class native_screen_recording_event_completed_t(Structure): + pass + + +class native_screen_recording_event_failed_t(Structure): + pass + + +class native_screen_recording_event_data_t(Union): + pass + + +class native_screen_recording_event_t(Structure): + pass + + class native_view_event_text_field_changed_t(Structure): pass @@ -300,6 +328,13 @@ class native_window_list_t(Structure): POINTER(native_application_event_t), c_void_p, ) +native_callback_bool_capture_source_payload_t = CFUNCTYPE( + None, + c_bool, + c_uint64, + c_uint64, + c_void_p, +) native_callback_bool_clipboard_data_payload_t = CFUNCTYPE( None, c_bool, @@ -360,6 +395,11 @@ class native_window_list_t(Structure): c_void_p, ) native_release_user_data_t = CFUNCTYPE(None, c_void_p) +native_screen_recording_event_callback_t = CFUNCTYPE( + None, + POINTER(native_screen_recording_event_t), + c_void_p, +) native_shortcut_event_callback_t = CFUNCTYPE( None, POINTER(native_shortcut_event_t), @@ -455,6 +495,36 @@ class native_window_list_t(Structure): ("type", c_int), ("data", native_notification_event_data_t), ] +native_capture_source_selection_options_t._fields_ = [ + ("allow_displays", c_bool), + ("allow_windows", c_bool), + ("parent_window", c_uint64), +] +native_screen_capture_options_t._fields_ = [ + ("include_cursor", c_bool), + ("crop_enabled", c_bool), + ("crop", native_rectangle_t), +] +native_screen_recording_options_t._fields_ = [ + ("capture", native_screen_capture_options_t), + ("frame_rate", c_double), + ("video_bitrate", c_ulong), +] +native_screen_recording_event_completed_t._fields_ = [ + ("file_path", c_char_p), + ("duration", c_double), +] +native_screen_recording_event_failed_t._fields_ = [ + ("message", c_char_p), +] +native_screen_recording_event_data_t._fields_ = [ + ("completed", native_screen_recording_event_completed_t), + ("failed", native_screen_recording_event_failed_t), +] +native_screen_recording_event_t._fields_ = [ + ("type", c_int), + ("data", native_screen_recording_event_data_t), +] native_view_event_text_field_changed_t._fields_ = [ ("text", c_char_p), ] @@ -1639,6 +1709,183 @@ class native_window_list_t(Structure): ], ) +# capture_source.h + +native_capture_source_free = function("native_capture_source_free", None, [c_uint64]) +native_capture_source_from_display = function( + "native_capture_source_from_display", + c_uint64, + [ + c_uint64, + ], +) +native_capture_source_from_window = function( + "native_capture_source_from_window", + c_uint64, + [ + c_uint64, + ], +) +native_capture_source_get_type = function( + "native_capture_source_get_type", + c_int, + [ + c_uint64, + ], +) +native_capture_source_get_name = function( + "native_capture_source_get_name", + c_void_p, + [ + c_uint64, + ], +) + +# screen_capturer.h + +native_capture_source_selection_options_free = function( + "native_capture_source_selection_options_free", + None, + [ + POINTER(native_capture_source_selection_options_t), + ], +) +native_screen_capture_options_free = function( + "native_screen_capture_options_free", + None, + [ + POINTER(native_screen_capture_options_t), + ], +) +native_screen_capturer_is_supported = function( + "native_screen_capturer_is_supported", + c_bool, + [ + ], +) +native_screen_capturer_is_source_selection_supported = function( + "native_screen_capturer_is_source_selection_supported", + c_bool, + [ + ], +) +native_screen_capturer_is_direct_capture_supported = function( + "native_screen_capturer_is_direct_capture_supported", + c_bool, + [ + c_int, + ], +) +native_screen_capturer_select_source = function( + "native_screen_capturer_select_source", + None, + [ + native_capture_source_selection_options_t, + native_callback_bool_capture_source_payload_t, + c_void_p, + native_release_user_data_t, + ], +) +native_screen_capturer_capture = function( + "native_screen_capturer_capture", + None, + [ + c_uint64, + native_screen_capture_options_t, + native_callback_bool_image_payload_t, + c_void_p, + native_release_user_data_t, + ], +) + +# screen_recorder.h + +native_screen_recording_options_free = function( + "native_screen_recording_options_free", + None, + [ + POINTER(native_screen_recording_options_t), + ], +) +native_screen_recorder_free = function("native_screen_recorder_free", None, [c_uint64]) +native_screen_recorder_create = function("native_screen_recorder_create", c_uint64, []) +native_screen_recorder_is_supported = function( + "native_screen_recorder_is_supported", + c_bool, + [ + ], +) +native_screen_recorder_start = function( + "native_screen_recorder_start", + c_bool, + [ + c_uint64, + c_uint64, + c_char_p, + native_screen_recording_options_t, + ], +) +native_screen_recorder_stop = function( + "native_screen_recorder_stop", + c_bool, + [ + c_uint64, + ], +) +native_screen_recorder_cancel = function( + "native_screen_recorder_cancel", + c_bool, + [ + c_uint64, + ], +) +native_screen_recorder_get_state = function( + "native_screen_recorder_get_state", + c_int, + [ + c_uint64, + ], +) +native_screen_recorder_is_active = function( + "native_screen_recorder_is_active", + c_bool, + [ + c_uint64, + ], +) +native_screen_recorder_get_duration = function( + "native_screen_recorder_get_duration", + c_double, + [ + c_uint64, + ], +) +native_screen_recorder_get_file_path = function( + "native_screen_recorder_get_file_path", + c_void_p, + [ + c_uint64, + ], +) +native_screen_recorder_add_listener = function( + "native_screen_recorder_add_listener", + c_uint64, + [ + c_uint64, + native_screen_recording_event_callback_t, + c_void_p, + native_release_user_data_t, + ], +) +native_screen_recorder_remove_listener = function( + "native_screen_recorder_remove_listener", + c_bool, + [ + c_uint64, + c_uint64, + ], +) + # window_shape.h native_window_shape_free = function("native_window_shape_free", None, [c_uint64]) diff --git a/bindings/python/nativeapi/capture_source.py b/bindings/python/nativeapi/capture_source.py new file mode 100644 index 00000000..a6cae08e --- /dev/null +++ b/bindings/python/nativeapi/capture_source.py @@ -0,0 +1,50 @@ +# AUTO-GENERATED. DO NOT EDIT. +# Any manual changes WILL BE LOST when this file is regenerated. +"""Generated from capture_source.h.""" + +from __future__ import annotations + +import enum + +from . import _capi as _C +from . import _runtime as _rt +from . import display as _display +from . import window as _window + + +class CaptureSourceType(enum.IntEnum): + NONE = 0 + DISPLAY = 1 + WINDOW = 2 + UNKNOWN = 3 + + +class CaptureSource(_rt.NativeObject): + """Owned reference to a native CaptureSource. + + `dispose()` (or `with`) releases it; otherwise it is released when the + wrapper is garbage collected. + """ + + __slots__ = () + _free = staticmethod(_C.native_capture_source_free) + + @staticmethod + def from_display(display: _display.Display | None) -> CaptureSource | None: + raw = _C.native_capture_source_from_display(_rt.handle_of(display)) + return CaptureSource._owned(raw) + + @staticmethod + def from_window(window: _window.Window | None) -> CaptureSource | None: + raw = _C.native_capture_source_from_window(_rt.handle_of(window)) + return CaptureSource._owned(raw) + + @property + def type(self) -> CaptureSourceType: + raw = _C.native_capture_source_get_type(self._handle) + return _rt.to_enum(CaptureSourceType, raw) + + @property + def name(self) -> str: + raw = _C.native_capture_source_get_name(self._handle) + return _rt.take_str(raw) diff --git a/bindings/python/nativeapi/screen_capturer.py b/bindings/python/nativeapi/screen_capturer.py new file mode 100644 index 00000000..ec04c18f --- /dev/null +++ b/bindings/python/nativeapi/screen_capturer.py @@ -0,0 +1,128 @@ +# AUTO-GENERATED. DO NOT EDIT. +# Any manual changes WILL BE LOST when this file is regenerated. +"""Generated from screen_capturer.h.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass, field + +from . import _capi as _C +from . import _runtime as _rt +from . import capture_source as _capture_source +from . import geometry as _geometry +from . import image as _image +from . import window as _window + + +@dataclass +class CaptureSourceSelectionOptions: + allow_displays: bool = False + allow_windows: bool = False + parent_window: _window.Window | None = None + + @classmethod + def _from_c( + cls, + raw: _C.native_capture_source_selection_options_t, + ) -> CaptureSourceSelectionOptions: + return cls( + bool(raw.allow_displays), + bool(raw.allow_windows), + _window.Window._owned(_rt.retain_handle(raw.parent_window)), + ) + + def _to_c(self) -> _C.native_capture_source_selection_options_t: + raw = _C.native_capture_source_selection_options_t() + raw.allow_displays = self.allow_displays + raw.allow_windows = self.allow_windows + raw.parent_window = _rt.handle_of(self.parent_window) + return raw + + +@dataclass +class ScreenCaptureOptions: + include_cursor: bool = False + crop_enabled: bool = False + crop: _geometry.Rectangle = field(default_factory=lambda: _geometry.Rectangle()) + + @classmethod + def _from_c(cls, raw: _C.native_screen_capture_options_t) -> ScreenCaptureOptions: + return cls( + bool(raw.include_cursor), + bool(raw.crop_enabled), + _geometry.Rectangle._from_c(raw.crop), + ) + + def _to_c(self) -> _C.native_screen_capture_options_t: + raw = _C.native_screen_capture_options_t() + raw.include_cursor = self.include_cursor + raw.crop_enabled = self.crop_enabled + raw.crop = self.crop._to_c() + return raw + + +class ScreenCapturer: + def __init__(self) -> None: + raise TypeError("ScreenCapturer contains only static methods") + + @staticmethod + def is_supported() -> bool: + raw = _C.native_screen_capturer_is_supported() + return raw + + @staticmethod + def is_source_selection_supported() -> bool: + raw = _C.native_screen_capturer_is_source_selection_supported() + return raw + + @staticmethod + def is_direct_capture_supported(type: _capture_source.CaptureSourceType) -> bool: + raw = _C.native_screen_capturer_is_direct_capture_supported(int(type)) + return raw + + @staticmethod + def select_source( + options: CaptureSourceSelectionOptions, + callback: Callable[[bool, _capture_source.CaptureSource | None], None], + ) -> None: + native_callback = _rt.make_callback( + _C.native_callback_bool_capture_source_payload_t, + lambda a0, a1, delivery, _user_data: _rt.deliver_callback( + delivery, + lambda: callback( + bool(a0), + _capture_source.CaptureSource._owned(_rt.retain_handle(a1)) + ), + ), + ) + _C.native_screen_capturer_select_source( + options._to_c(), + native_callback, + _rt.user_data(native_callback), + _rt.release_user_data, + ) + + @staticmethod + def capture( + source: _capture_source.CaptureSource | None, + options: ScreenCaptureOptions, + callback: Callable[[bool, _image.Image | None], None], + ) -> None: + native_callback = _rt.make_callback( + _C.native_callback_bool_image_payload_t, + lambda a0, a1, delivery, _user_data: _rt.deliver_callback( + delivery, + lambda: callback( + bool(a0), + _image.Image._owned(_rt.retain_handle(a1)) + ), + ), + ) + _C.native_screen_capturer_capture( + _rt.handle_of(source), + options._to_c(), + native_callback, + _rt.user_data(native_callback), + _rt.release_user_data, + ) diff --git a/bindings/python/nativeapi/screen_recorder.py b/bindings/python/nativeapi/screen_recorder.py new file mode 100644 index 00000000..d2634ce8 --- /dev/null +++ b/bindings/python/nativeapi/screen_recorder.py @@ -0,0 +1,184 @@ +# AUTO-GENERATED. DO NOT EDIT. +# Any manual changes WILL BE LOST when this file is regenerated. +"""Generated from screen_recorder.h.""" + +from __future__ import annotations + +import enum +from collections.abc import Callable +from dataclasses import dataclass, field + +from . import _capi as _C +from . import _runtime as _rt +from . import capture_source as _capture_source +from . import screen_capturer as _screen_capturer + + +class ScreenRecordingState(enum.IntEnum): + IDLE = 0 + STARTING = 1 + RECORDING = 2 + STOPPING = 3 + COMPLETED = 4 + CANCELLED = 5 + FAILED = 6 + + +@dataclass +class ScreenRecordingOptions: + capture: _screen_capturer.ScreenCaptureOptions = ( + field(default_factory=lambda: _screen_capturer.ScreenCaptureOptions()) + ) + frame_rate: float = 0.0 + video_bitrate: int = 0 + + @classmethod + def _from_c( + cls, + raw: _C.native_screen_recording_options_t, + ) -> ScreenRecordingOptions: + return cls( + _screen_capturer.ScreenCaptureOptions._from_c(raw.capture), + raw.frame_rate, + raw.video_bitrate, + ) + + def _to_c(self) -> _C.native_screen_recording_options_t: + raw = _C.native_screen_recording_options_t() + raw.capture = self.capture._to_c() + raw.frame_rate = self.frame_rate + raw.video_bitrate = self.video_bitrate + return raw + + +@dataclass(frozen=True) +class ScreenRecordingEvent: + """Base of every ScreenRecordingEvent; listeners receive one of its subclasses.""" + + @staticmethod + def _from_c(raw: _C.native_screen_recording_event_t) -> ScreenRecordingEvent | None: + if raw.type == 0: + return ScreenRecordingStartedEvent() + if raw.type == 1: + return ScreenRecordingCompletedEvent( + _rt.decode(raw.data.completed.file_path), + raw.data.completed.duration, + ) + if raw.type == 2: + return ScreenRecordingCancelledEvent() + if raw.type == 3: + return ScreenRecordingFailedEvent(_rt.decode(raw.data.failed.message)) + return None + + +@dataclass(frozen=True) +class ScreenRecordingStartedEvent(ScreenRecordingEvent): + pass + + +@dataclass(frozen=True) +class ScreenRecordingCompletedEvent(ScreenRecordingEvent): + file_path: str + duration: float + + +@dataclass(frozen=True) +class ScreenRecordingCancelledEvent(ScreenRecordingEvent): + pass + + +@dataclass(frozen=True) +class ScreenRecordingFailedEvent(ScreenRecordingEvent): + message: str + + +class ScreenRecorder(_rt.NativeObject): + """Owned reference to a native ScreenRecorder. + + `dispose()` (or `with`) releases it; otherwise it is released when the + wrapper is garbage collected. + """ + + __slots__ = () + _free = staticmethod(_C.native_screen_recorder_free) + + def __init__(self) -> None: + handle = _C.native_screen_recorder_create() + if not handle: + raise _rt.NativeApiError("failed to create a ScreenRecorder") + self._adopt(handle) + + @staticmethod + def is_supported() -> bool: + raw = _C.native_screen_recorder_is_supported() + return raw + + def start( + self, + source: _capture_source.CaptureSource | None, + file_path: str, + options: ScreenRecordingOptions, + ) -> bool: + raw = _C.native_screen_recorder_start( + self._handle, + _rt.handle_of(source), + _rt.encode(file_path), + options._to_c(), + ) + return raw + + def stop(self) -> bool: + raw = _C.native_screen_recorder_stop(self._handle) + return raw + + def cancel(self) -> bool: + raw = _C.native_screen_recorder_cancel(self._handle) + return raw + + @property + def state(self) -> ScreenRecordingState: + raw = _C.native_screen_recorder_get_state(self._handle) + return _rt.to_enum(ScreenRecordingState, raw) + + @property + def is_active(self) -> bool: + raw = _C.native_screen_recorder_is_active(self._handle) + return raw + + @property + def duration(self) -> float: + raw = _C.native_screen_recorder_get_duration(self._handle) + return raw + + @property + def file_path(self) -> str: + raw = _C.native_screen_recorder_get_file_path(self._handle) + return _rt.take_str(raw) + + def add_listener(self, callback: Callable[[ScreenRecordingEvent], None]) -> int: + """Calls `callback` with every ScreenRecordingEvent this ScreenRecorder emits. + + Callbacks run synchronously; coroutine callbacks are not accepted. + Use an owned EventDecision for asynchronous request confirmation. + Returns the listener id for `remove_listener()`. + """ + + def trampoline(raw, _user_data): + event = ScreenRecordingEvent._from_c(raw.contents) + if event is not None: + _rt.deliver_event(callback, event) + + return _rt.add_listener( + _C.native_screen_recorder_add_listener, + _C.native_screen_recording_event_callback_t, + trampoline, + self._handle, + ) + + def remove_listener(self, listener_id: int) -> bool: + """Unregisters a listener. Returns False if unknown.""" + return _rt.remove_listener( + _C.native_screen_recorder_remove_listener, + listener_id, + self._handle, + ) diff --git a/bindings/python/tests/test_screen_capture.py b/bindings/python/tests/test_screen_capture.py new file mode 100644 index 00000000..6b13ca9b --- /dev/null +++ b/bindings/python/tests/test_screen_capture.py @@ -0,0 +1,35 @@ +"""No capture/UI: validate nested options and recorder state across the real ABI.""" + +import pytest + +from nativeapi import ( + CaptureSourceType, + Rectangle, + ScreenCaptureOptions, + ScreenCapturer, + ScreenRecorder, + ScreenRecordingOptions, + ScreenRecordingState, +) + + +def test_static_entry_and_invalid_direct_types(): + with pytest.raises(TypeError): + ScreenCapturer() + assert not ScreenCapturer.is_direct_capture_supported(CaptureSourceType.NONE) + assert not ScreenCapturer.is_direct_capture_supported(CaptureSourceType.UNKNOWN) + + +def test_nested_options_rejection_keeps_idle_recorder(): + capture = ScreenCaptureOptions(False, True, Rectangle(1, 2, 32, 48)) + options = ScreenRecordingOptions(capture, 30.0, 1_000_000) + # Exercises the actual nested ctypes value conversion independently of a + # desktop/permission; a relative destination is rejected before any capture. + with ScreenRecorder() as recorder: + assert recorder.state == ScreenRecordingState.IDLE + assert not recorder.start(None, "relative.mp4", options) + assert recorder.state == ScreenRecordingState.IDLE + assert recorder.duration == 0 + assert not recorder.is_active + assert not recorder.stop() + assert not recorder.cancel() diff --git a/bindings/rust/cnativeapi/build.rs b/bindings/rust/cnativeapi/build.rs index b9ac4bd0..65a8b841 100644 --- a/bindings/rust/cnativeapi/build.rs +++ b/bindings/rust/cnativeapi/build.rs @@ -34,6 +34,39 @@ fn main() { match target_os.as_str() { "macos" => { cmake_config.define("CMAKE_SYSTEM_NAME", "Darwin"); + // Match Rust's deployment target so availability-annotated native + // APIs remain weak imports on older macOS versions. + println!("cargo:rerun-if-env-changed=MACOSX_DEPLOYMENT_TARGET"); + let deployment_target = env::var("MACOSX_DEPLOYMENT_TARGET") + .ok() + .or_else(|| { + let output = std::process::Command::new(env::var("RUSTC").ok()?) + .args([ + "--print", + "deployment-target", + "--target", + &env::var("TARGET").ok()?, + ]) + .output() + .ok()?; + if !output.status.success() { + return None; + } + String::from_utf8(output.stdout) + .ok()? + .trim() + .split_once('=') + .map(|(_, value)| value.to_owned()) + }) + .unwrap_or_else(|| { + if _target_arch == "aarch64" { + "11.0" + } else { + "10.12" + } + .into() + }); + cmake_config.define("CMAKE_OSX_DEPLOYMENT_TARGET", deployment_target); // Mirrors src/CMakeLists.txt. Cocoa alone leaves the global // shortcut and launch-at-login implementations unresolved: hotkeys // go through Carbon's RegisterEventHotKey, and login items through @@ -42,6 +75,11 @@ fn main() { println!("cargo:rustc-link-lib=framework=QuartzCore"); println!("cargo:rustc-link-lib=framework=Carbon"); println!("cargo:rustc-link-lib=framework=ServiceManagement"); + // SCK is autolinked by the capture object, so an application not + // using capture does not acquire an unconditional SCK dependency. + for framework in ["AVFoundation", "CoreMedia", "CoreVideo", "VideoToolbox"] { + println!("cargo:rustc-link-lib=framework={framework}"); + } } "ios" => { cmake_config.define("CMAKE_SYSTEM_NAME", "iOS"); @@ -63,7 +101,22 @@ fn main() { println!("cargo:rustc-link-lib=dwmapi"); println!("cargo:rustc-link-lib=gdiplus"); println!("cargo:rustc-link-lib=crypt32"); - for library in ["comctl32", "advapi32", "version", "ole32", "uuid", "gdi32"] { + for library in [ + "comctl32", + "advapi32", + "version", + "ole32", + "uuid", + "gdi32", + "d3d11", + "dxgi", + "windowsapp", + "runtimeobject", + "mfplat", + "mfreadwrite", + "mfuuid", + "windowscodecs", + ] { println!("cargo:rustc-link-lib={library}"); } } diff --git a/bindings/rust/cnativeapi/src/bindings.rs b/bindings/rust/cnativeapi/src/bindings.rs index 03624860..1d84ff4f 100644 --- a/bindings/rust/cnativeapi/src/bindings.rs +++ b/bindings/rust/cnativeapi/src/bindings.rs @@ -128,12 +128,28 @@ pub struct native_edge_insets_t { pub bottom: f64, pub left: f64, } +unsafe extern "C" { + #[doc = " Frees everything the struct owns."] + pub fn native_point_free(value: *mut native_point_t); +} +unsafe extern "C" { + #[doc = " Frees everything the struct owns."] + pub fn native_size_free(value: *mut native_size_t); +} +unsafe extern "C" { + #[doc = " Frees everything the struct owns."] + pub fn native_rectangle_free(value: *mut native_rectangle_t); +} unsafe extern "C" { pub fn native_edge_insets_all(value: f64) -> native_edge_insets_t; } unsafe extern "C" { pub fn native_edge_insets_symmetric(vertical: f64, horizontal: f64) -> native_edge_insets_t; } +unsafe extern "C" { + #[doc = " Frees everything the struct owns."] + pub fn native_edge_insets_free(value: *mut native_edge_insets_t); +} unsafe extern "C" { #[doc = " Caller owns the returned handle; release it with native_image_free()."] pub fn native_image_from_file(file_path: *const ::std::os::raw::c_char) -> native_image_t; @@ -332,6 +348,10 @@ unsafe extern "C" { unsafe extern "C" { pub fn native_color_to_argb(color: native_color_t) -> ::std::os::raw::c_uint; } +unsafe extern "C" { + #[doc = " Frees everything the struct owns."] + pub fn native_color_free(value: *mut native_color_t); +} pub type native_view_id_t = ::std::os::raw::c_uint; pub const NATIVE_VIEW_LAYOUT_ABSOLUTE: native_view_layout_t = 0; pub const NATIVE_VIEW_LAYOUT_ROW: native_view_layout_t = 1; @@ -1986,6 +2006,150 @@ unsafe extern "C" { #[doc = " Unregisters a listener. Returns false if unknown."] pub fn native_application_remove_listener(listener_id: native_listener_id_t) -> bool; } +#[doc = " Opaque Display handle.\n\n A generational index into the library's handle table, NOT a pointer:\n never dereference it, and compare it against NATIVE_INVALID_DISPLAY rather than NULL.\n Releasing a handle invalidates it; later calls fail safely instead of\n touching freed memory."] +pub type native_display_t = u64; +pub type native_display_id_t = ::std::os::raw::c_uint; +pub const NATIVE_DISPLAY_ORIENTATION_PORTRAIT: native_display_orientation_t = 0; +pub const NATIVE_DISPLAY_ORIENTATION_LANDSCAPE: native_display_orientation_t = 90; +pub const NATIVE_DISPLAY_ORIENTATION_PORTRAIT_FLIPPED: native_display_orientation_t = 180; +pub const NATIVE_DISPLAY_ORIENTATION_LANDSCAPE_FLIPPED: native_display_orientation_t = 270; +pub type native_display_orientation_t = ::std::os::raw::c_uint; +#[doc = " Owning list of Display handles."] +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct native_display_list_t { + pub displays: *mut native_display_t, + pub count: ::std::os::raw::c_long, +} +impl Default for native_display_list_t { + fn default() -> Self { + let mut s = ::std::mem::MaybeUninit::::uninit(); + unsafe { + ::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1); + s.assume_init() + } + } +} +pub const NATIVE_DISPLAY_EVENT_TYPE_ADDED: native_display_event_type_t = 0; +pub const NATIVE_DISPLAY_EVENT_TYPE_REMOVED: native_display_event_type_t = 1; +pub const NATIVE_DISPLAY_EVENT_TYPE_CHANGED: native_display_event_type_t = 2; +#[doc = " Which concrete DisplayEvent arrived."] +pub type native_display_event_type_t = ::std::os::raw::c_uint; +#[doc = " One DisplayEvent, tagged by its concrete type.\n\n Synchronous callbacks borrow this payload until they return. Async callbacks\n borrow it until event_delivery_complete. Copy anything needed after that."] +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct native_display_event_t { + pub type_: native_display_event_type_t, + pub display: native_display_t, +} +impl Default for native_display_event_t { + fn default() -> Self { + let mut s = ::std::mem::MaybeUninit::::uninit(); + unsafe { + ::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1); + s.assume_init() + } + } +} +pub type native_display_event_callback_t = ::std::option::Option< + unsafe extern "C" fn( + event: *const native_display_event_t, + user_data: *mut ::std::os::raw::c_void, + ), +>; +pub type native_display_event_callback_t_async = ::std::option::Option< + unsafe extern "C" fn( + event: *const native_display_event_t, + delivery: native_event_delivery_t, + user_data: *mut ::std::os::raw::c_void, + ), +>; +unsafe extern "C" { + #[doc = " Creates a Display instance; release it with native_display_free()."] + pub fn native_display_create(display: *mut ::std::os::raw::c_void) -> native_display_t; +} +unsafe extern "C" { + pub fn native_display_get_id(display: native_display_t) -> native_display_id_t; +} +unsafe extern "C" { + #[doc = " Caller owns the returned string; free it with free_c_str()."] + pub fn native_display_get_name(display: native_display_t) -> *mut ::std::os::raw::c_char; +} +unsafe extern "C" { + pub fn native_display_get_position(display: native_display_t) -> native_point_t; +} +unsafe extern "C" { + pub fn native_display_get_size(display: native_display_t) -> native_size_t; +} +unsafe extern "C" { + pub fn native_display_get_work_area(display: native_display_t) -> native_rectangle_t; +} +unsafe extern "C" { + pub fn native_display_get_scale_factor(display: native_display_t) -> f64; +} +unsafe extern "C" { + pub fn native_display_is_primary(display: native_display_t) -> bool; +} +unsafe extern "C" { + pub fn native_display_get_orientation( + display: native_display_t, + ) -> native_display_orientation_t; +} +unsafe extern "C" { + pub fn native_display_get_refresh_rate(display: native_display_t) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + pub fn native_display_get_bit_depth(display: native_display_t) -> ::std::os::raw::c_int; +} +unsafe extern "C" { + #[doc = " Platform-specific native object (NSScreen*, HMONITOR, ...)."] + pub fn native_display_get_native_object( + display: native_display_t, + ) -> *mut ::std::os::raw::c_void; +} +unsafe extern "C" { + #[doc = " Releases the caller's reference. Safe to call with an invalid or\n already-released handle."] + pub fn native_display_free(display: native_display_t); +} +unsafe extern "C" { + #[doc = " Frees the array and releases every handle it contains."] + pub fn native_display_list_free(list: *mut native_display_list_t); +} +unsafe extern "C" { + #[doc = " Frees only the array; the caller takes over the handles."] + pub fn native_display_list_release(list: *mut native_display_list_t); +} +pub const NATIVE_CAPTURE_SOURCE_TYPE_NONE: native_capture_source_type_t = 0; +pub const NATIVE_CAPTURE_SOURCE_TYPE_DISPLAY: native_capture_source_type_t = 1; +pub const NATIVE_CAPTURE_SOURCE_TYPE_WINDOW: native_capture_source_type_t = 2; +pub const NATIVE_CAPTURE_SOURCE_TYPE_UNKNOWN: native_capture_source_type_t = 3; +pub type native_capture_source_type_t = ::std::os::raw::c_uint; +#[doc = " Opaque CaptureSource handle.\n\n A generational index into the library's handle table, NOT a pointer:\n never dereference it, and compare it against NATIVE_INVALID_CAPTURE_SOURCE rather than NULL.\n Releasing a handle invalidates it; later calls fail safely instead of\n touching freed memory."] +pub type native_capture_source_t = u64; +unsafe extern "C" { + #[doc = " Caller owns the returned handle; release it with native_capture_source_free()."] + pub fn native_capture_source_from_display(display: native_display_t) + -> native_capture_source_t; +} +unsafe extern "C" { + #[doc = " Caller owns the returned handle; release it with native_capture_source_free()."] + pub fn native_capture_source_from_window(window: native_window_t) -> native_capture_source_t; +} +unsafe extern "C" { + pub fn native_capture_source_get_type( + capture_source: native_capture_source_t, + ) -> native_capture_source_type_t; +} +unsafe extern "C" { + #[doc = " Caller owns the returned string; free it with free_c_str()."] + pub fn native_capture_source_get_name( + capture_source: native_capture_source_t, + ) -> *mut ::std::os::raw::c_char; +} +unsafe extern "C" { + #[doc = " Releases the caller's reference. Safe to call with an invalid or\n already-released handle."] + pub fn native_capture_source_free(capture_source: native_capture_source_t); +} #[doc = " An owning list of strings.\n\n Free with native_string_list_free(); it releases every item and the array."] #[repr(C)] #[derive(Debug, Copy, Clone)] @@ -2242,119 +2406,6 @@ pub const NATIVE_DIALOG_MODALITY_NONE: native_dialog_modality_t = 0; pub const NATIVE_DIALOG_MODALITY_APPLICATION: native_dialog_modality_t = 1; pub const NATIVE_DIALOG_MODALITY_WINDOW: native_dialog_modality_t = 2; pub type native_dialog_modality_t = ::std::os::raw::c_uint; -pub type native_display_id_t = ::std::os::raw::c_uint; -pub const NATIVE_DISPLAY_ORIENTATION_PORTRAIT: native_display_orientation_t = 0; -pub const NATIVE_DISPLAY_ORIENTATION_LANDSCAPE: native_display_orientation_t = 90; -pub const NATIVE_DISPLAY_ORIENTATION_PORTRAIT_FLIPPED: native_display_orientation_t = 180; -pub const NATIVE_DISPLAY_ORIENTATION_LANDSCAPE_FLIPPED: native_display_orientation_t = 270; -pub type native_display_orientation_t = ::std::os::raw::c_uint; -#[doc = " Opaque Display handle.\n\n A generational index into the library's handle table, NOT a pointer:\n never dereference it, and compare it against NATIVE_INVALID_DISPLAY rather than NULL.\n Releasing a handle invalidates it; later calls fail safely instead of\n touching freed memory."] -pub type native_display_t = u64; -#[doc = " Owning list of Display handles."] -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct native_display_list_t { - pub displays: *mut native_display_t, - pub count: ::std::os::raw::c_long, -} -impl Default for native_display_list_t { - fn default() -> Self { - let mut s = ::std::mem::MaybeUninit::::uninit(); - unsafe { - ::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1); - s.assume_init() - } - } -} -pub const NATIVE_DISPLAY_EVENT_TYPE_ADDED: native_display_event_type_t = 0; -pub const NATIVE_DISPLAY_EVENT_TYPE_REMOVED: native_display_event_type_t = 1; -pub const NATIVE_DISPLAY_EVENT_TYPE_CHANGED: native_display_event_type_t = 2; -#[doc = " Which concrete DisplayEvent arrived."] -pub type native_display_event_type_t = ::std::os::raw::c_uint; -#[doc = " One DisplayEvent, tagged by its concrete type.\n\n Synchronous callbacks borrow this payload until they return. Async callbacks\n borrow it until event_delivery_complete. Copy anything needed after that."] -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct native_display_event_t { - pub type_: native_display_event_type_t, - pub display: native_display_t, -} -impl Default for native_display_event_t { - fn default() -> Self { - let mut s = ::std::mem::MaybeUninit::::uninit(); - unsafe { - ::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1); - s.assume_init() - } - } -} -pub type native_display_event_callback_t = ::std::option::Option< - unsafe extern "C" fn( - event: *const native_display_event_t, - user_data: *mut ::std::os::raw::c_void, - ), ->; -pub type native_display_event_callback_t_async = ::std::option::Option< - unsafe extern "C" fn( - event: *const native_display_event_t, - delivery: native_event_delivery_t, - user_data: *mut ::std::os::raw::c_void, - ), ->; -unsafe extern "C" { - #[doc = " Creates a Display instance; release it with native_display_free()."] - pub fn native_display_create(display: *mut ::std::os::raw::c_void) -> native_display_t; -} -unsafe extern "C" { - pub fn native_display_get_id(display: native_display_t) -> native_display_id_t; -} -unsafe extern "C" { - #[doc = " Caller owns the returned string; free it with free_c_str()."] - pub fn native_display_get_name(display: native_display_t) -> *mut ::std::os::raw::c_char; -} -unsafe extern "C" { - pub fn native_display_get_position(display: native_display_t) -> native_point_t; -} -unsafe extern "C" { - pub fn native_display_get_size(display: native_display_t) -> native_size_t; -} -unsafe extern "C" { - pub fn native_display_get_work_area(display: native_display_t) -> native_rectangle_t; -} -unsafe extern "C" { - pub fn native_display_get_scale_factor(display: native_display_t) -> f64; -} -unsafe extern "C" { - pub fn native_display_is_primary(display: native_display_t) -> bool; -} -unsafe extern "C" { - pub fn native_display_get_orientation( - display: native_display_t, - ) -> native_display_orientation_t; -} -unsafe extern "C" { - pub fn native_display_get_refresh_rate(display: native_display_t) -> ::std::os::raw::c_int; -} -unsafe extern "C" { - pub fn native_display_get_bit_depth(display: native_display_t) -> ::std::os::raw::c_int; -} -unsafe extern "C" { - #[doc = " Platform-specific native object (NSScreen*, HMONITOR, ...)."] - pub fn native_display_get_native_object( - display: native_display_t, - ) -> *mut ::std::os::raw::c_void; -} -unsafe extern "C" { - #[doc = " Releases the caller's reference. Safe to call with an invalid or\n already-released handle."] - pub fn native_display_free(display: native_display_t); -} -unsafe extern "C" { - #[doc = " Frees the array and releases every handle it contains."] - pub fn native_display_list_free(list: *mut native_display_list_t); -} -unsafe extern "C" { - #[doc = " Frees only the array; the caller takes over the handles."] - pub fn native_display_list_release(list: *mut native_display_list_t); -} unsafe extern "C" { pub fn native_display_manager_get_all() -> native_display_list_t; } @@ -3151,6 +3202,243 @@ unsafe extern "C" { #[doc = " Releases the caller's reference. Safe to call with an invalid or\n already-released handle."] pub fn native_preferences_free(preferences: native_preferences_t); } +#[doc = " Arguments are borrowed until event_delivery_complete(delivery, ...), which is required exactly once."] +pub type native_screen_capturer_select_source_callback_t = ::std::option::Option< + unsafe extern "C" fn( + arg0: bool, + arg1: native_capture_source_t, + delivery: native_event_delivery_t, + user_data: *mut ::std::os::raw::c_void, + ), +>; +#[doc = " Arguments are borrowed until event_delivery_complete(delivery, ...), which is required exactly once."] +pub type native_screen_capturer_capture_callback_t = ::std::option::Option< + unsafe extern "C" fn( + arg0: bool, + arg1: native_image_t, + delivery: native_event_delivery_t, + user_data: *mut ::std::os::raw::c_void, + ), +>; +#[repr(C)] +#[derive(Debug, Default, Copy, Clone)] +pub struct native_capture_source_selection_options_t { + pub allow_displays: bool, + pub allow_windows: bool, + pub parent_window: native_window_t, +} +#[repr(C)] +#[derive(Debug, Default, Copy, Clone)] +pub struct native_screen_capture_options_t { + pub include_cursor: bool, + pub crop_enabled: bool, + pub crop: native_rectangle_t, +} +unsafe extern "C" { + #[doc = " Frees everything the struct owns."] + pub fn native_capture_source_selection_options_free( + value: *mut native_capture_source_selection_options_t, + ); +} +unsafe extern "C" { + #[doc = " Frees everything the struct owns."] + pub fn native_screen_capture_options_free(value: *mut native_screen_capture_options_t); +} +unsafe extern "C" { + pub fn native_screen_capturer_is_supported() -> bool; +} +unsafe extern "C" { + pub fn native_screen_capturer_is_source_selection_supported() -> bool; +} +unsafe extern "C" { + pub fn native_screen_capturer_is_direct_capture_supported( + type_: native_capture_source_type_t, + ) -> bool; +} +unsafe extern "C" { + pub fn native_screen_capturer_select_source( + options: native_capture_source_selection_options_t, + callback: native_screen_capturer_select_source_callback_t, + callback_user_data: *mut ::std::os::raw::c_void, + callback_release_user_data: native_release_user_data_t, + ); +} +unsafe extern "C" { + pub fn native_screen_capturer_capture( + source: native_capture_source_t, + options: native_screen_capture_options_t, + callback: native_screen_capturer_capture_callback_t, + callback_user_data: *mut ::std::os::raw::c_void, + callback_release_user_data: native_release_user_data_t, + ); +} +pub const NATIVE_SCREEN_RECORDING_STATE_IDLE: native_screen_recording_state_t = 0; +pub const NATIVE_SCREEN_RECORDING_STATE_STARTING: native_screen_recording_state_t = 1; +pub const NATIVE_SCREEN_RECORDING_STATE_RECORDING: native_screen_recording_state_t = 2; +pub const NATIVE_SCREEN_RECORDING_STATE_STOPPING: native_screen_recording_state_t = 3; +pub const NATIVE_SCREEN_RECORDING_STATE_COMPLETED: native_screen_recording_state_t = 4; +pub const NATIVE_SCREEN_RECORDING_STATE_CANCELLED: native_screen_recording_state_t = 5; +pub const NATIVE_SCREEN_RECORDING_STATE_FAILED: native_screen_recording_state_t = 6; +pub type native_screen_recording_state_t = ::std::os::raw::c_uint; +#[repr(C)] +#[derive(Debug, Default, Copy, Clone)] +pub struct native_screen_recording_options_t { + pub capture: native_screen_capture_options_t, + pub frame_rate: f64, + pub video_bitrate: ::std::os::raw::c_ulong, +} +#[doc = " Opaque ScreenRecorder handle.\n\n A generational index into the library's handle table, NOT a pointer:\n never dereference it, and compare it against NATIVE_INVALID_SCREEN_RECORDER rather than NULL.\n Releasing a handle invalidates it; later calls fail safely instead of\n touching freed memory."] +pub type native_screen_recorder_t = u64; +pub const NATIVE_SCREEN_RECORDING_EVENT_TYPE_STARTED: native_screen_recording_event_type_t = 0; +pub const NATIVE_SCREEN_RECORDING_EVENT_TYPE_COMPLETED: native_screen_recording_event_type_t = 1; +pub const NATIVE_SCREEN_RECORDING_EVENT_TYPE_CANCELLED: native_screen_recording_event_type_t = 2; +pub const NATIVE_SCREEN_RECORDING_EVENT_TYPE_FAILED: native_screen_recording_event_type_t = 3; +#[doc = " Which concrete ScreenRecordingEvent arrived."] +pub type native_screen_recording_event_type_t = ::std::os::raw::c_uint; +#[doc = " One ScreenRecordingEvent, tagged by its concrete type.\n\n Synchronous callbacks borrow this payload until they return. Async callbacks\n borrow it until event_delivery_complete. Copy anything needed after that."] +#[repr(C)] +#[derive(Copy, Clone)] +pub struct native_screen_recording_event_t { + pub type_: native_screen_recording_event_type_t, + pub data: native_screen_recording_event_t__bindgen_ty_1, +} +#[repr(C)] +#[derive(Copy, Clone)] +pub union native_screen_recording_event_t__bindgen_ty_1 { + pub completed: native_screen_recording_event_t__bindgen_ty_1__bindgen_ty_1, + pub failed: native_screen_recording_event_t__bindgen_ty_1__bindgen_ty_2, +} +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct native_screen_recording_event_t__bindgen_ty_1__bindgen_ty_1 { + pub file_path: *mut ::std::os::raw::c_char, + pub duration: f64, +} +impl Default for native_screen_recording_event_t__bindgen_ty_1__bindgen_ty_1 { + fn default() -> Self { + let mut s = ::std::mem::MaybeUninit::::uninit(); + unsafe { + ::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1); + s.assume_init() + } + } +} +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct native_screen_recording_event_t__bindgen_ty_1__bindgen_ty_2 { + pub message: *mut ::std::os::raw::c_char, +} +impl Default for native_screen_recording_event_t__bindgen_ty_1__bindgen_ty_2 { + fn default() -> Self { + let mut s = ::std::mem::MaybeUninit::::uninit(); + unsafe { + ::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1); + s.assume_init() + } + } +} +impl Default for native_screen_recording_event_t__bindgen_ty_1 { + fn default() -> Self { + let mut s = ::std::mem::MaybeUninit::::uninit(); + unsafe { + ::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1); + s.assume_init() + } + } +} +impl Default for native_screen_recording_event_t { + fn default() -> Self { + let mut s = ::std::mem::MaybeUninit::::uninit(); + unsafe { + ::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1); + s.assume_init() + } + } +} +pub type native_screen_recording_event_callback_t = ::std::option::Option< + unsafe extern "C" fn( + event: *const native_screen_recording_event_t, + user_data: *mut ::std::os::raw::c_void, + ), +>; +pub type native_screen_recording_event_callback_t_async = ::std::option::Option< + unsafe extern "C" fn( + event: *const native_screen_recording_event_t, + delivery: native_event_delivery_t, + user_data: *mut ::std::os::raw::c_void, + ), +>; +unsafe extern "C" { + #[doc = " Frees everything the struct owns."] + pub fn native_screen_recording_options_free(value: *mut native_screen_recording_options_t); +} +unsafe extern "C" { + #[doc = " Creates a ScreenRecorder instance; release it with native_screen_recorder_free()."] + pub fn native_screen_recorder_create() -> native_screen_recorder_t; +} +unsafe extern "C" { + pub fn native_screen_recorder_is_supported() -> bool; +} +unsafe extern "C" { + pub fn native_screen_recorder_start( + screen_recorder: native_screen_recorder_t, + source: native_capture_source_t, + file_path: *const ::std::os::raw::c_char, + options: native_screen_recording_options_t, + ) -> bool; +} +unsafe extern "C" { + pub fn native_screen_recorder_stop(screen_recorder: native_screen_recorder_t) -> bool; +} +unsafe extern "C" { + pub fn native_screen_recorder_cancel(screen_recorder: native_screen_recorder_t) -> bool; +} +unsafe extern "C" { + pub fn native_screen_recorder_get_state( + screen_recorder: native_screen_recorder_t, + ) -> native_screen_recording_state_t; +} +unsafe extern "C" { + pub fn native_screen_recorder_is_active(screen_recorder: native_screen_recorder_t) -> bool; +} +unsafe extern "C" { + pub fn native_screen_recorder_get_duration(screen_recorder: native_screen_recorder_t) -> f64; +} +unsafe extern "C" { + #[doc = " Caller owns the returned string; free it with free_c_str()."] + pub fn native_screen_recorder_get_file_path( + screen_recorder: native_screen_recorder_t, + ) -> *mut ::std::os::raw::c_char; +} +unsafe extern "C" { + #[doc = " Releases the caller's reference. Safe to call with an invalid or\n already-released handle."] + pub fn native_screen_recorder_free(screen_recorder: native_screen_recorder_t); +} +unsafe extern "C" { + #[doc = " Registers @p callback for every ScreenRecordingEvent this ScreenRecorder emits.\n @return the listener id, or NATIVE_INVALID_LISTENER_ID on failure."] + pub fn native_screen_recorder_add_listener( + screen_recorder: native_screen_recorder_t, + callback: native_screen_recording_event_callback_t, + user_data: *mut ::std::os::raw::c_void, + release_user_data: native_release_user_data_t, + ) -> native_listener_id_t; +} +unsafe extern "C" { + #[doc = " Registers an asynchronous callback. Its event, borrowed handles and user_data\n remain valid until event_delivery_complete is called, including after removal.\n Every delivered payload must be acknowledged. Check is_active before invoking a queued callback."] + pub fn native_screen_recorder_add_listener_async( + screen_recorder: native_screen_recorder_t, + callback: native_screen_recording_event_callback_t_async, + user_data: *mut ::std::os::raw::c_void, + release_user_data: native_release_user_data_t, + ) -> native_listener_id_t; +} +unsafe extern "C" { + #[doc = " Unregisters a listener. Returns false if unknown."] + pub fn native_screen_recorder_remove_listener( + screen_recorder: native_screen_recorder_t, + listener_id: native_listener_id_t, + ) -> bool; +} #[doc = " Opaque SecureStorage handle.\n\n A generational index into the library's handle table, NOT a pointer:\n never dereference it, and compare it against NATIVE_INVALID_SECURE_STORAGE rather than NULL.\n Releasing a handle invalidates it; later calls fail safely instead of\n touching freed memory."] pub type native_secure_storage_t = u64; unsafe extern "C" { diff --git a/bindings/rust/nativeapi/src/capture_source.rs b/bindings/rust/nativeapi/src/capture_source.rs new file mode 100644 index 00000000..e2cd4a8e --- /dev/null +++ b/bindings/rust/nativeapi/src/capture_source.rs @@ -0,0 +1,122 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. +#![allow(dead_code)] +#![allow(unused_imports)] + +use cnativeapi; +use std::ffi::{CStr, CString}; + +use crate::display::Display; +use crate::window::Window; + +#[repr(i32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum CaptureSourceType { + None = 0, + Display = 1, + Window = 2, + Unknown = 3, +} + +impl CaptureSourceType { + pub(crate) fn from_raw(raw: cnativeapi::native_capture_source_type_t) -> Self { + match raw { + cnativeapi::NATIVE_CAPTURE_SOURCE_TYPE_NONE => Self::None, + cnativeapi::NATIVE_CAPTURE_SOURCE_TYPE_DISPLAY => Self::Display, + cnativeapi::NATIVE_CAPTURE_SOURCE_TYPE_WINDOW => Self::Window, + cnativeapi::NATIVE_CAPTURE_SOURCE_TYPE_UNKNOWN => Self::Unknown, + _ => Self::None, + } + } + + pub(crate) fn to_raw(self) -> cnativeapi::native_capture_source_type_t { + self as cnativeapi::native_capture_source_type_t + } +} + +/// Owned handle to a native `CaptureSource`. +#[derive(Debug)] +#[repr(transparent)] +pub struct CaptureSource { + handle: cnativeapi::native_capture_source_t, +} + +impl CaptureSource { + /// Adopts a handle returned by the C API. + /// + /// # Safety + /// `handle` must be a live handle that is not owned elsewhere. + pub unsafe fn from_raw(handle: cnativeapi::native_capture_source_t) -> Option { + if handle == 0 { + None + } else { + Some(Self { handle }) + } + } + + /// Borrows the underlying C handle. + pub fn as_raw(&self) -> cnativeapi::native_capture_source_t { + self.handle + } + + pub fn from_display(display: Option<&Display>) -> Option { + unsafe { + CaptureSource::from_raw(cnativeapi::native_capture_source_from_display(display.map_or(0, |value| value.as_raw()))) + } + } + + pub fn from_window(window: Option<&Window>) -> Option { + unsafe { + CaptureSource::from_raw(cnativeapi::native_capture_source_from_window(window.map_or(0, |value| value.as_raw()))) + } + } + + pub fn r#type(&self) -> CaptureSourceType { + unsafe { + CaptureSourceType::from_raw(cnativeapi::native_capture_source_get_type(self.handle)) + } + } + + pub fn name(&self) -> Option { + unsafe { + let ptr = cnativeapi::native_capture_source_get_name(self.handle); + if ptr.is_null() { + return None; + } + let value = CStr::from_ptr(ptr).to_string_lossy().into_owned(); + cnativeapi::free_c_str(ptr); + Some(value) + } + } + +} + +impl Drop for CaptureSource { + fn drop(&mut self) { + if self.handle != 0 { + unsafe { cnativeapi::native_capture_source_free(self.handle) }; + } + } +} + +/// A `CaptureSource` owned elsewhere; using it does not release it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CaptureSourceRef(cnativeapi::native_capture_source_t); + +impl CaptureSourceRef { + pub(crate) fn from_raw(handle: cnativeapi::native_capture_source_t) -> Self { + Self(handle) + } + + pub fn as_raw(&self) -> cnativeapi::native_capture_source_t { + self.0 + } + + /// Runs `f` against the borrowed handle. The `CaptureSource` it receives + /// must not outlive the call. + pub fn with(&self, f: impl FnOnce(&CaptureSource) -> R) -> R { + let borrowed = std::mem::ManuallyDrop::new(CaptureSource { handle: self.0 }); + f(&borrowed) + } +} + diff --git a/bindings/rust/nativeapi/src/modules.rs b/bindings/rust/nativeapi/src/modules.rs index 53fd70a6..92ded2de 100644 --- a/bindings/rust/nativeapi/src/modules.rs +++ b/bindings/rust/nativeapi/src/modules.rs @@ -6,6 +6,7 @@ pub mod accessibility_manager; pub mod app_info; pub mod application; +pub mod capture_source; pub mod clipboard; pub mod color; pub mod device_info; @@ -27,6 +28,8 @@ pub mod notification_manager; pub mod placement; pub mod positioning_strategy; pub mod preferences; +pub mod screen_capturer; +pub mod screen_recorder; pub mod secure_storage; pub mod shortcut; pub mod shortcut_manager; @@ -45,6 +48,7 @@ pub mod window_shape; pub use accessibility_manager::{AccessibilityManager}; pub use app_info::{AppInfo}; pub use application::{Brightness, ApplicationEvent, Application}; +pub use capture_source::{CaptureSourceType, CaptureSource, CaptureSourceRef}; pub use clipboard::{ClipboardData, ClipboardEvent, Clipboard}; pub use color::{Color}; pub use device_info::{DeviceInfo}; @@ -66,6 +70,8 @@ pub use notification_manager::{NotificationEvent, NotificationManager}; pub use placement::{Placement}; pub use positioning_strategy::{PositioningStrategyType, PositioningStrategy, PositioningStrategyRef}; pub use preferences::{Preferences, PreferencesRef}; +pub use screen_capturer::{CaptureSourceSelectionOptions, ScreenCaptureOptions, ScreenCapturer}; +pub use screen_recorder::{ScreenRecordingState, ScreenRecordingOptions, ScreenRecordingEvent, ScreenRecorder, ScreenRecorderRef}; pub use secure_storage::{SecureStorage, SecureStorageRef}; pub use shortcut::{ShortcutId, ShortcutScope, ShortcutOptions, ShortcutEvent, Shortcut, ShortcutRef}; pub use shortcut_manager::{ShortcutManager}; diff --git a/bindings/rust/nativeapi/src/screen_capturer.rs b/bindings/rust/nativeapi/src/screen_capturer.rs new file mode 100644 index 00000000..38e53c2b --- /dev/null +++ b/bindings/rust/nativeapi/src/screen_capturer.rs @@ -0,0 +1,153 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. +#![allow(dead_code)] +#![allow(unused_imports)] + +use cnativeapi; +use std::ffi::{CStr, CString}; + +use crate::capture_source::{CaptureSource, CaptureSourceType}; +use crate::geometry::{RawOfRectangle, Rectangle}; +use crate::image::Image; +use crate::window::Window; + +#[derive(Debug)] +pub struct CaptureSourceSelectionOptions { + pub allow_displays: bool, + pub allow_windows: bool, + pub parent_window: Option, +} + +impl CaptureSourceSelectionOptions { + pub(crate) unsafe fn from_raw(raw: &cnativeapi::native_capture_source_selection_options_t) -> Self { + Self { + allow_displays: raw.allow_displays, + allow_windows: raw.allow_windows, + parent_window: Window::from_raw(cnativeapi::native_handle_retain(raw.parent_window)), + } + } + + pub(crate) fn to_raw(&self) -> RawOfCaptureSourceSelectionOptions { + let mut raw = cnativeapi::native_capture_source_selection_options_t::default(); + raw.allow_displays = self.allow_displays; + raw.allow_windows = self.allow_windows; + raw.parent_window = self.parent_window.as_ref().map_or(0, |v| v.as_raw()); + RawOfCaptureSourceSelectionOptions { raw, _owned: () } + } +} + +/// `CaptureSourceSelectionOptions` in its C form, keeping any borrowed buffers alive. +pub(crate) struct RawOfCaptureSourceSelectionOptions { + pub(crate) raw: cnativeapi::native_capture_source_selection_options_t, + _owned: (), +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ScreenCaptureOptions { + pub include_cursor: bool, + pub crop_enabled: bool, + pub crop: Rectangle, +} + +impl ScreenCaptureOptions { + pub(crate) unsafe fn from_raw(raw: &cnativeapi::native_screen_capture_options_t) -> Self { + Self { + include_cursor: raw.include_cursor, + crop_enabled: raw.crop_enabled, + crop: Rectangle::from_raw(&raw.crop), + } + } + + pub(crate) fn to_raw(&self) -> RawOfScreenCaptureOptions { + let mut raw = cnativeapi::native_screen_capture_options_t::default(); + raw.include_cursor = self.include_cursor; + raw.crop_enabled = self.crop_enabled; + let crop_nested = self.crop.to_raw(); + raw.crop = crop_nested.raw; + RawOfScreenCaptureOptions { raw, _owned: (crop_nested, ()) } + } +} + +/// `ScreenCaptureOptions` in its C form, keeping any borrowed buffers alive. +pub(crate) struct RawOfScreenCaptureOptions { + pub(crate) raw: cnativeapi::native_screen_capture_options_t, + _owned: (RawOfRectangle, ()), +} + +pub struct ScreenCapturer; + +impl ScreenCapturer { + pub fn is_supported() -> bool { + unsafe { + cnativeapi::native_screen_capturer_is_supported() + } + } + + pub fn is_source_selection_supported() -> bool { + unsafe { + cnativeapi::native_screen_capturer_is_source_selection_supported() + } + } + + pub fn is_direct_capture_supported(r#type: CaptureSourceType) -> bool { + unsafe { + cnativeapi::native_screen_capturer_is_direct_capture_supported(r#type.to_raw()) + } + } + + pub fn select_source(options: &CaptureSourceSelectionOptions, callback: impl Fn(bool, Option) + 'static) { + let options_raw = options.to_raw(); + unsafe extern "C" fn trampoline(arg0: bool, arg1: u64, delivery: cnativeapi::native_event_delivery_t, user_data: *mut std::ffi::c_void) { + struct Delivery(cnativeapi::native_event_delivery_t); + impl Drop for Delivery { fn drop(&mut self) { unsafe { cnativeapi::native_event_delivery_complete(self.0, true); } } } + let _delivery = Delivery(delivery); + if !cnativeapi::native_event_delivery_is_active(delivery) { return; } + if user_data.is_null() { + return; + } + let callback = &*(user_data as *const std::sync::Arc)>); + let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| callback(arg0, CaptureSource::from_raw(cnativeapi::native_handle_retain(arg1))))); + } + fn into_user_data(callback: std::sync::Arc)>) -> *mut std::ffi::c_void { + Box::into_raw(Box::new(callback)) as *mut std::ffi::c_void + } + unsafe extern "C" fn release(user_data: *mut std::ffi::c_void) { + if !user_data.is_null() { + drop(Box::from_raw(user_data as *mut std::sync::Arc)>)); + } + } + let callback_user_data = into_user_data(std::sync::Arc::new(callback)); + unsafe { + cnativeapi::native_screen_capturer_select_source(options_raw.raw, Some(trampoline), callback_user_data, Some(release)); + } + } + + pub fn capture(source: Option<&CaptureSource>, options: &ScreenCaptureOptions, callback: impl Fn(bool, Option) + 'static) { + let options_raw = options.to_raw(); + unsafe extern "C" fn trampoline(arg0: bool, arg1: u64, delivery: cnativeapi::native_event_delivery_t, user_data: *mut std::ffi::c_void) { + struct Delivery(cnativeapi::native_event_delivery_t); + impl Drop for Delivery { fn drop(&mut self) { unsafe { cnativeapi::native_event_delivery_complete(self.0, true); } } } + let _delivery = Delivery(delivery); + if !cnativeapi::native_event_delivery_is_active(delivery) { return; } + if user_data.is_null() { + return; + } + let callback = &*(user_data as *const std::sync::Arc)>); + let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| callback(arg0, Image::from_raw(cnativeapi::native_handle_retain(arg1))))); + } + fn into_user_data(callback: std::sync::Arc)>) -> *mut std::ffi::c_void { + Box::into_raw(Box::new(callback)) as *mut std::ffi::c_void + } + unsafe extern "C" fn release(user_data: *mut std::ffi::c_void) { + if !user_data.is_null() { + drop(Box::from_raw(user_data as *mut std::sync::Arc)>)); + } + } + let callback_user_data = into_user_data(std::sync::Arc::new(callback)); + unsafe { + cnativeapi::native_screen_capturer_capture(source.map_or(0, |value| value.as_raw()), options_raw.raw, Some(trampoline), callback_user_data, Some(release)); + } + } + +} + diff --git a/bindings/rust/nativeapi/src/screen_recorder.rs b/bindings/rust/nativeapi/src/screen_recorder.rs new file mode 100644 index 00000000..060eb5d4 --- /dev/null +++ b/bindings/rust/nativeapi/src/screen_recorder.rs @@ -0,0 +1,244 @@ +// AUTO-GENERATED. DO NOT EDIT. +// Any manual changes WILL BE LOST when this file is regenerated. +#![allow(dead_code)] +#![allow(unused_imports)] + +use cnativeapi; +use std::ffi::{CStr, CString}; + +use crate::capture_source::CaptureSource; +use crate::screen_capturer::{RawOfScreenCaptureOptions, ScreenCaptureOptions}; + +/// Identifies one registered event listener. +pub type ListenerId = cnativeapi::native_listener_id_t; + +#[repr(i32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ScreenRecordingState { + Idle = 0, + Starting = 1, + Recording = 2, + Stopping = 3, + Completed = 4, + Cancelled = 5, + Failed = 6, +} + +impl ScreenRecordingState { + pub(crate) fn from_raw(raw: cnativeapi::native_screen_recording_state_t) -> Self { + match raw { + cnativeapi::NATIVE_SCREEN_RECORDING_STATE_IDLE => Self::Idle, + cnativeapi::NATIVE_SCREEN_RECORDING_STATE_STARTING => Self::Starting, + cnativeapi::NATIVE_SCREEN_RECORDING_STATE_RECORDING => Self::Recording, + cnativeapi::NATIVE_SCREEN_RECORDING_STATE_STOPPING => Self::Stopping, + cnativeapi::NATIVE_SCREEN_RECORDING_STATE_COMPLETED => Self::Completed, + cnativeapi::NATIVE_SCREEN_RECORDING_STATE_CANCELLED => Self::Cancelled, + cnativeapi::NATIVE_SCREEN_RECORDING_STATE_FAILED => Self::Failed, + _ => Self::Idle, + } + } + + pub(crate) fn to_raw(self) -> cnativeapi::native_screen_recording_state_t { + self as cnativeapi::native_screen_recording_state_t + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ScreenRecordingOptions { + pub capture: ScreenCaptureOptions, + pub frame_rate: f64, + pub video_bitrate: std::os::raw::c_ulong, +} + +impl ScreenRecordingOptions { + pub(crate) unsafe fn from_raw(raw: &cnativeapi::native_screen_recording_options_t) -> Self { + Self { + capture: ScreenCaptureOptions::from_raw(&raw.capture), + frame_rate: raw.frame_rate, + video_bitrate: raw.video_bitrate, + } + } + + pub(crate) fn to_raw(&self) -> RawOfScreenRecordingOptions { + let mut raw = cnativeapi::native_screen_recording_options_t::default(); + let capture_nested = self.capture.to_raw(); + raw.capture = capture_nested.raw; + raw.frame_rate = self.frame_rate; + raw.video_bitrate = self.video_bitrate; + RawOfScreenRecordingOptions { raw, _owned: (capture_nested, ()) } + } +} + +/// `ScreenRecordingOptions` in its C form, keeping any borrowed buffers alive. +pub(crate) struct RawOfScreenRecordingOptions { + pub(crate) raw: cnativeapi::native_screen_recording_options_t, + _owned: (RawOfScreenCaptureOptions, ()), +} + +/// One `ScreenRecordingEvent`, in its concrete form. +#[derive(Debug, Clone, PartialEq)] +pub enum ScreenRecordingEvent { + Started, + Completed { file_path: Option, duration: f64 }, + Cancelled, + Failed { message: Option }, +} + +impl ScreenRecordingEvent { + pub(crate) unsafe fn from_raw(raw: &cnativeapi::native_screen_recording_event_t) -> Option { + Some(match raw.type_ { + cnativeapi::NATIVE_SCREEN_RECORDING_EVENT_TYPE_STARTED => Self::Started, + cnativeapi::NATIVE_SCREEN_RECORDING_EVENT_TYPE_COMPLETED => Self::Completed { file_path: if raw.data.completed.file_path.is_null() { None } else { Some(CStr::from_ptr(raw.data.completed.file_path).to_string_lossy().into_owned()) }, duration: raw.data.completed.duration }, + cnativeapi::NATIVE_SCREEN_RECORDING_EVENT_TYPE_CANCELLED => Self::Cancelled, + cnativeapi::NATIVE_SCREEN_RECORDING_EVENT_TYPE_FAILED => Self::Failed { message: if raw.data.failed.message.is_null() { None } else { Some(CStr::from_ptr(raw.data.failed.message).to_string_lossy().into_owned()) } }, + _ => return None, + }) + } +} + +/// Owned handle to a native `ScreenRecorder`. +#[derive(Debug)] +#[repr(transparent)] +pub struct ScreenRecorder { + handle: cnativeapi::native_screen_recorder_t, +} + +impl ScreenRecorder { + /// Adopts a handle returned by the C API. + /// + /// # Safety + /// `handle` must be a live handle that is not owned elsewhere. + pub unsafe fn from_raw(handle: cnativeapi::native_screen_recorder_t) -> Option { + if handle == 0 { + None + } else { + Some(Self { handle }) + } + } + + /// Borrows the underlying C handle. + pub fn as_raw(&self) -> cnativeapi::native_screen_recorder_t { + self.handle + } + + /// Creates a new `ScreenRecorder`; returns `None` if the native side failed. + pub fn new() -> Option { + unsafe { + Self::from_raw(cnativeapi::native_screen_recorder_create()) + } + } + + pub fn is_supported() -> bool { + unsafe { + cnativeapi::native_screen_recorder_is_supported() + } + } + + pub fn start(&self, source: Option<&CaptureSource>, file_path: &str, options: &ScreenRecordingOptions) -> bool { + let file_path_native = CString::new(file_path).expect("string argument contains interior nul byte"); + let options_raw = options.to_raw(); + unsafe { + cnativeapi::native_screen_recorder_start(self.handle, source.map_or(0, |value| value.as_raw()), file_path_native.as_ptr(), options_raw.raw) + } + } + + pub fn stop(&self) -> bool { + unsafe { + cnativeapi::native_screen_recorder_stop(self.handle) + } + } + + pub fn cancel(&self) -> bool { + unsafe { + cnativeapi::native_screen_recorder_cancel(self.handle) + } + } + + pub fn state(&self) -> ScreenRecordingState { + unsafe { + ScreenRecordingState::from_raw(cnativeapi::native_screen_recorder_get_state(self.handle)) + } + } + + pub fn is_active(&self) -> bool { + unsafe { + cnativeapi::native_screen_recorder_is_active(self.handle) + } + } + + pub fn duration(&self) -> f64 { + unsafe { + cnativeapi::native_screen_recorder_get_duration(self.handle) + } + } + + pub fn file_path(&self) -> Option { + unsafe { + let ptr = cnativeapi::native_screen_recorder_get_file_path(self.handle); + if ptr.is_null() { + return None; + } + let value = CStr::from_ptr(ptr).to_string_lossy().into_owned(); + cnativeapi::free_c_str(ptr); + Some(value) + } + } + + /// Registers `callback` for every `ScreenRecordingEvent` this `ScreenRecorder` emits. + /// + /// The closure is dropped on the main thread once the listener is removed + /// or its emitter destroyed. + pub fn add_listener(&self, callback: impl Fn(&ScreenRecordingEvent) + 'static) -> ListenerId { + unsafe extern "C" fn trampoline(event: *const cnativeapi::native_screen_recording_event_t, user_data: *mut std::ffi::c_void) { + if event.is_null() || user_data.is_null() { + return; + } + let callback = &*(user_data as *const Box); + if let Some(event) = ScreenRecordingEvent::from_raw(&*event) { + callback(&event); + } + } + let boxed: Box> = Box::new(Box::new(callback)); + let user_data = Box::into_raw(boxed) as *mut std::ffi::c_void; + unsafe extern "C" fn release(user_data: *mut std::ffi::c_void) { + drop(Box::from_raw(user_data as *mut Box)); + } + unsafe { cnativeapi::native_screen_recorder_add_listener(self.handle, Some(trampoline), user_data, Some(release)) } + } + + /// Unregisters a listener. Returns false if unknown. + pub fn remove_listener(&self, listener_id: ListenerId) -> bool { + unsafe { cnativeapi::native_screen_recorder_remove_listener(self.handle, listener_id) } + } + +} + +impl Drop for ScreenRecorder { + fn drop(&mut self) { + if self.handle != 0 { + unsafe { cnativeapi::native_screen_recorder_free(self.handle) }; + } + } +} + +/// A `ScreenRecorder` owned elsewhere; using it does not release it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ScreenRecorderRef(cnativeapi::native_screen_recorder_t); + +impl ScreenRecorderRef { + pub(crate) fn from_raw(handle: cnativeapi::native_screen_recorder_t) -> Self { + Self(handle) + } + + pub fn as_raw(&self) -> cnativeapi::native_screen_recorder_t { + self.0 + } + + /// Runs `f` against the borrowed handle. The `ScreenRecorder` it receives + /// must not outlive the call. + pub fn with(&self, f: impl FnOnce(&ScreenRecorder) -> R) -> R { + let borrowed = std::mem::ManuallyDrop::new(ScreenRecorder { handle: self.0 }); + f(&borrowed) + } +} + diff --git a/bindings/rust/nativeapi/tests/screen_capture.rs b/bindings/rust/nativeapi/tests/screen_capture.rs new file mode 100644 index 00000000..7f4914e6 --- /dev/null +++ b/bindings/rust/nativeapi/tests/screen_capture.rs @@ -0,0 +1,37 @@ +use nativeapi::{ + CaptureSourceType, Rectangle, ScreenCaptureOptions, ScreenCapturer, ScreenRecorder, + ScreenRecordingOptions, ScreenRecordingState, +}; + +// No desktop access: links the real capture backend and exercises nested FFI +// values, source validation and lifecycle rejection without requesting capture. +#[test] +fn invalid_request_keeps_recorder_idle() { + assert!(!ScreenCapturer::is_direct_capture_supported( + CaptureSourceType::None + )); + assert!(!ScreenCapturer::is_direct_capture_supported( + CaptureSourceType::Unknown + )); + let options = ScreenRecordingOptions { + capture: ScreenCaptureOptions { + include_cursor: false, + crop_enabled: true, + crop: Rectangle { + x: 1.0, + y: 2.0, + width: 32.0, + height: 48.0, + }, + }, + frame_rate: 30.0, + video_bitrate: 1_000_000, + }; + let recorder = ScreenRecorder::new().expect("recorder handle"); + assert!(!recorder.start(None, "relative.mp4", &options)); + assert_eq!(recorder.state(), ScreenRecordingState::Idle); + assert_eq!(recorder.duration(), 0.0); + assert!(!recorder.is_active()); + assert!(!recorder.stop()); + assert!(!recorder.cancel()); +} diff --git a/core b/core index 5086bdb7..ecdf94e9 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 5086bdb7238f48822665b676c6284edece7db305 +Subproject commit ecdf94e9dbd8ddc8f74f43ae7e31154c96da2795 diff --git a/examples/flutter_screen_capture_example/.gitignore b/examples/flutter_screen_capture_example/.gitignore new file mode 100644 index 00000000..79f7ecab --- /dev/null +++ b/examples/flutter_screen_capture_example/.gitignore @@ -0,0 +1,48 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# Widget Preview related +.widget_preview/ diff --git a/examples/flutter_screen_capture_example/.metadata b/examples/flutter_screen_capture_example/.metadata new file mode 100644 index 00000000..12f6820b --- /dev/null +++ b/examples/flutter_screen_capture_example/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "5fc346839b5d0eef006ed8404392afb4dfae428d" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 5fc346839b5d0eef006ed8404392afb4dfae428d + base_revision: 5fc346839b5d0eef006ed8404392afb4dfae428d + - platform: macos + create_revision: 5fc346839b5d0eef006ed8404392afb4dfae428d + base_revision: 5fc346839b5d0eef006ed8404392afb4dfae428d + - platform: windows + create_revision: 5fc346839b5d0eef006ed8404392afb4dfae428d + base_revision: 5fc346839b5d0eef006ed8404392afb4dfae428d + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/examples/flutter_screen_capture_example/README.md b/examples/flutter_screen_capture_example/README.md new file mode 100644 index 00000000..2c6b5602 --- /dev/null +++ b/examples/flutter_screen_capture_example/README.md @@ -0,0 +1,52 @@ +# Screen capture example + +Flutter desktop example for `ScreenCapturer`, `CaptureSource`, and `ScreenRecorder`. +Requires macOS 14+ or Windows 10 version 2004+ with an available H.264 encoder. +Other platforms currently expose unsupported stubs. + +## Run + +From the workspace root: + +```sh +flutter pub get +cd examples/flutter_screen_capture_example +flutter run -d macos +# On Windows: +flutter run -d windows +``` + +## Try it + +1. Select **This window**, a display, or **System picker…**. On Windows the + system picker lets the user choose either a window or a display. +2. Optionally include the cursor or crop a rectangle. Crop coordinates are + physical pixels relative to the selected source, not desktop coordinates. +3. **Take screenshot** shows a preview; **Save PNG** writes a separate file. +4. **Start recording**, then **Stop & save** to finalize a silent H.264 MP4. + **Discard** cancels and removes the temporary recording. +5. Choose an output directory to keep captures outside the default temporary + directory. Each output receives a unique filename; existing files are not + overwritten. + +Allow the operating system's screen-capture request when prompted. Direct +capture on macOS may require Screen & System Audio Recording permission; +use the system picker to select a source explicitly. Closing the app during an +operation is blocked until it finishes or the recording is discarded. + +The controller keeps native source/image/recorder handles alive until completion +and explicitly disposes them. Capture options are supplied explicitly because +binding defaults do not yet consistently match the C++ defaults. The screenshot +preview decodes the data URI returned by `Image.toBase64()`. + +## Validation and remaining work + +The macOS example has been built and run: actual screenshots (including a +2560 × 1440 frame) and short MP4 recordings succeeded. The Windows core capture +backend has desktop smoke coverage; the Flutter example has not yet been run +on Windows. Longer recordings, permission denial, source disappearance and +runtime shutdown still need end-to-end coverage. + +The current **Open folder**, **Open** and **Play** actions use `UrlOpener`, whose +current implementation accepts HTTP/HTTPS only. Local files are saved correctly, +but these actions report an error; open the displayed path manually for now. diff --git a/examples/flutter_screen_capture_example/analysis_options.yaml b/examples/flutter_screen_capture_example/analysis_options.yaml new file mode 100644 index 00000000..362ff73a --- /dev/null +++ b/examples/flutter_screen_capture_example/analysis_options.yaml @@ -0,0 +1,34 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +analyzer: + exclude: + - build/** + - windows/** + - macos/** + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/examples/flutter_screen_capture_example/lib/capture_controller.dart b/examples/flutter_screen_capture_example/lib/capture_controller.dart new file mode 100644 index 00000000..f5fe922a --- /dev/null +++ b/examples/flutter_screen_capture_example/lib/capture_controller.dart @@ -0,0 +1,406 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:file_selector/file_selector.dart'; +import 'package:flutter/foundation.dart'; +import 'package:nativeapi/nativeapi.dart' as na; + +class CaptureArtifact { + const CaptureArtifact(this.path, {this.duration, this.width, this.height}); + final String path; + final double? duration; + final int? width; + final int? height; + bool get isVideo => duration != null; + String get name => path.split(Platform.pathSeparator).last; +} + +/// Owns native handles explicitly. UI changes never alter an in-flight request. +class CaptureController extends ChangeNotifier { + final artifacts = []; + final log = []; + List displays = []; + na.Window? _window; + na.CaptureSource? _source; + na.Image? _image; + na.ScreenRecorder? _recorder; + int? _recordingListener, _closeListener, _quitListener; + Timer? _timer; + final _elapsed = Stopwatch(); + bool _disposed = false; + bool ready = false; + bool busy = false; + bool captureSupported = false; + bool recordingSupported = false; + bool pickerSupported = false; + String sourceLabel = 'No source selected'; + String outputDirectory = ''; + String status = 'Choose a display, this window, or the system picker.'; + String? error; + Uint8List? preview; + int imageWidth = 0, imageHeight = 0; + na.ScreenRecordingState recordingState = na.ScreenRecordingState.idle; + + bool get hasSource => _source != null; + bool get hasImage => _image != null; + bool get active => switch (recordingState) { + na.ScreenRecordingState.starting || + na.ScreenRecordingState.recording || + na.ScreenRecordingState.stopping => true, + _ => false, + }; + bool get locked => busy || active || !ready; + double get elapsed => _elapsed.elapsedMilliseconds / 1000; + + void _changed() { + if (!_disposed) notifyListeners(); + } + + void _message(String message, {bool failure = false}) { + status = message; + error = failure ? message : null; + log.insert(0, message); + if (log.length > 30) log.removeLast(); + debugPrint('[capture] $message'); + _changed(); + } + + Future initialize() async { + try { + final directory = await Directory.systemTemp.createTemp( + 'nativeapi-capture-', + ); + if (_disposed) return; + outputDirectory = directory.path; + _window = na.WindowManager.instance.getCurrent(); + _window?.contentSize = const na.Size(width: 1120, height: 800); + captureSupported = na.ScreenCapturer.isSupported(); + recordingSupported = na.ScreenRecorder.isSupported(); + pickerSupported = na.ScreenCapturer.isSourceSelectionSupported(); + displays = na.DisplayManager.instance.getAll(); + _closeListener = _window?.addListener((event) { + if (event is na.WindowCloseRequestedEvent && (busy || active)) { + event.request.cancel(); + _message( + 'Finish the current action, or stop / discard the recording before closing.', + ); + } + }); + _quitListener = na.Application.instance.addListener((event) { + if (event is na.ApplicationQuitRequestedEvent && (busy || active)) { + event.request.cancel(); + _message('Stop and save, or discard the recording before quitting.'); + } + }); + ready = true; + _changed(); + } catch (e) { + _message('Initialization failed: $e', failure: true); + } + } + + void _replaceSource(na.CaptureSource? next) { + if (next == null) { + _message( + 'This source is unavailable. Select it again or try the system picker.', + failure: true, + ); + return; + } + _source?.dispose(); + _source = next; + final name = next.name; + sourceLabel = name == null || name.isEmpty + ? 'Selected ${next.type.name} source' + : name; + _message('Source selected: $sourceLabel'); + } + + void selectDisplay(na.Display display) { + if (locked) return; + _replaceSource(na.CaptureSource.fromDisplay(display)); + } + + void selectThisWindow() { + if (locked) return; + _replaceSource(na.CaptureSource.fromWindow(_window)); + } + + void releaseSource() { + if (locked) return; + _source?.dispose(); + _source = null; + sourceLabel = 'No source selected'; + _message('Source released. Any retained sharing session can now close.'); + } + + Future selectSource() async { + if (locked || !pickerSupported) return; + busy = true; + _message('Choose one screen or window in the system picker.'); + try { + final result = Completer<(bool, na.CaptureSource?)>(); + na.ScreenCapturer.selectSource( + na.CaptureSourceSelectionOptions( + allowDisplays: true, + allowWindows: true, + parentWindow: _window, + ), + (success, source) => result.complete((success, source)), + ); + final (success, source) = await result.future; + if (_disposed) { + source?.dispose(); + return; + } + if (!success) { + source?.dispose(); + _message( + 'Source selection failed. Check system permission and try again.', + failure: true, + ); + } else if (source == null) { + _message('Selection cancelled. The previous source is unchanged.'); + } else { + _replaceSource(source); + } + } catch (e) { + _message('Source selection failed: $e', failure: true); + } finally { + busy = false; + _changed(); + } + } + + Future capture(na.ScreenCaptureOptions options) async { + if (locked || _source == null) return; + busy = true; + _message('Capturing a new frame…'); + try { + final result = Completer<(bool, na.Image?)>(); + na.ScreenCapturer.capture( + _source, + options, + (success, image) => result.complete((success, image)), + ); + final (success, image) = await result.future; + if (_disposed) { + image?.dispose(); + return; + } + if (!success || image == null) { + image?.dispose(); + throw StateError( + 'No frame received. Check screen-recording permission, source availability and crop bounds.', + ); + } + try { + final encoded = image.toBase64(); + if (encoded == null) { + throw StateError('Cannot convert the captured image for preview.'); + } + // Image.ToBase64 returns a data URI, including its MIME type. + final bytes = encoded.startsWith('data:') + ? UriData.parse(encoded).contentAsBytes() + : base64Decode(encoded); + imageWidth = image.size.width.toInt(); + imageHeight = image.size.height.toInt(); + _image?.dispose(); + _image = image; + preview = bytes; + } catch (_) { + image.dispose(); + rethrow; + } + _message('Screenshot ready · $imageWidth × $imageHeight px'); + } catch (e) { + _message('Screenshot failed: $e', failure: true); + } finally { + busy = false; + _changed(); + } + } + + Future _newPath(String extension) async { + if (outputDirectory.isEmpty || !await Directory(outputDirectory).exists()) { + throw StateError('Choose an existing output folder.'); + } + final path = + '$outputDirectory${Platform.pathSeparator}capture-${DateTime.now().microsecondsSinceEpoch}.$extension'; + if (await FileSystemEntity.type(path) != FileSystemEntityType.notFound) { + throw StateError( + 'The output already exists. Try again with a new filename.', + ); + } + return path; + } + + Future saveScreenshot() async { + if (locked || _image == null) return; + busy = true; + _changed(); + try { + final path = await _newPath('png'); + if (_disposed) return; + if (!_image!.saveToFile(path)) { + throw StateError('PNG write failed. Check folder permissions.'); + } + artifacts.insert( + 0, + CaptureArtifact(path, width: imageWidth, height: imageHeight), + ); + _message('PNG saved: $path'); + } catch (e) { + _message('Save failed: $e', failure: true); + } finally { + busy = false; + _changed(); + } + } + + Future chooseOutputDirectory() async { + if (locked) return; + busy = true; + _changed(); + try { + final path = await getDirectoryPath(confirmButtonText: 'Use this folder'); + if (_disposed) return; + if (path != null) { + outputDirectory = path; + _message('Output folder: $path'); + } + } catch (e) { + _message('Cannot select a folder: $e', failure: true); + } finally { + busy = false; + _changed(); + } + } + + Future start(na.ScreenRecordingOptions options) async { + if (locked || _source == null || !recordingSupported) return; + busy = true; + _changed(); + try { + final path = await _newPath('mp4'); + if (_disposed) return; + final recorder = na.ScreenRecorder.create(); + if (recorder == null) throw StateError('Cannot create a recorder.'); + _recorder = recorder; + _recordingListener = recorder.addListener((event) { + if (_disposed || !identical(_recorder, recorder)) return; + if (event is na.ScreenRecordingStartedEvent) { + // Queued Started must not turn an already-cancelled session back on. + recordingState = recorder.state; + if (recordingState == na.ScreenRecordingState.recording) { + _elapsed.start(); + _timer = Timer.periodic( + const Duration(milliseconds: 200), + (_) => _changed(), + ); + _message('Recording · MP4 / H.264 · no audio'); + } + } else if (event is na.ScreenRecordingCompletedEvent) { + recordingState = na.ScreenRecordingState.completed; + final saved = event.filePath; + if (saved != null) { + artifacts.insert( + 0, + CaptureArtifact(saved, duration: event.duration), + ); + } + _message( + 'Recording saved · ${event.duration.toStringAsFixed(1)} s · $saved', + ); + _releaseRecorder(); + } else if (event is na.ScreenRecordingCancelledEvent) { + recordingState = na.ScreenRecordingState.cancelled; + _message('Recording discarded. No video file was saved.'); + _releaseRecorder(); + } else if (event is na.ScreenRecordingFailedEvent) { + recordingState = na.ScreenRecordingState.failed; + _message( + 'Recording failed: ${event.message ?? 'Unknown native failure'}', + failure: true, + ); + _releaseRecorder(); + } + _changed(); + }); + if (_recordingListener == 0) { + throw StateError('Cannot subscribe to recording events.'); + } + _elapsed.reset(); + recordingState = na.ScreenRecordingState.starting; + if (!recorder.start(_source, path, options)) { + recordingState = na.ScreenRecordingState.idle; + throw StateError( + 'Start rejected. Check the source, options and output path.', + ); + } + _message('Starting… waiting for permission and the first encoded frame.'); + } catch (e) { + _releaseRecorder(); + recordingState = na.ScreenRecordingState.failed; + _message('Cannot start recording: $e', failure: true); + } finally { + busy = false; + _changed(); + } + } + + void stop() { + if (_recorder?.stop() != true) return; + recordingState = na.ScreenRecordingState.stopping; + _elapsed.stop(); + _message('Saving… waiting for the MP4 to be finalized.'); + } + + void cancel() { + if (_recorder?.cancel() != true) return; + recordingState = na.ScreenRecordingState.stopping; + _elapsed.stop(); + _message('Discarding… waiting for temporary output cleanup.'); + } + + void openPath(String path, {bool directory = false}) { + final uri = directory ? Uri.directory(path) : Uri.file(path); + final result = na.UrlOpener.instance.open(uri.toString()); + if (!result.success) { + _message('Cannot open $path: ${result.errorMessage}', failure: true); + } + } + + void reportInvalidOptions(String message) => _message(message, failure: true); + + void _releaseRecorder() { + _timer?.cancel(); + _timer = null; + _elapsed.stop(); + if (_recordingListener != null) { + _recorder?.removeListener(_recordingListener!); + } + _recordingListener = null; + _recorder?.dispose(); + _recorder = null; + } + + @override + void dispose() { + _disposed = true; + // Ordinary close is vetoed while active; forced teardown discards output. + _releaseRecorder(); + if (_closeListener != null) _window?.removeListener(_closeListener!); + if (_quitListener != null) { + na.Application.instance.removeListener(_quitListener!); + } + _image?.dispose(); + _source?.dispose(); + for (final display in displays) { + display.dispose(); + } + _window?.dispose(); + super.dispose(); + } +} diff --git a/examples/flutter_screen_capture_example/lib/main.dart b/examples/flutter_screen_capture_example/lib/main.dart new file mode 100644 index 00000000..75561694 --- /dev/null +++ b/examples/flutter_screen_capture_example/lib/main.dart @@ -0,0 +1,589 @@ +import 'dart:async'; + +import 'package:dazzui_host/dazzui_host.dart'; +import 'package:nativeapi/nativeapi.dart' as na; + +import 'capture_controller.dart'; + +void main() => runApp(const CaptureApp()); + +class CaptureApp extends StatelessWidget { + const CaptureApp({super.key, this.controller}); + final CaptureController? controller; + + @override + Widget build(BuildContext context) => Host( + title: 'Screen Capture', + home: CapturePage(controller: controller), + ); +} + +class CapturePage extends StatefulWidget { + const CapturePage({super.key, this.controller}); + final CaptureController? controller; + @override + State createState() => _CapturePageState(); +} + +class _CapturePageState extends State { + late final CaptureController capture; + final cropFields = [ + TextEditingController(text: '0'), + TextEditingController(text: '0'), + TextEditingController(text: '640'), + TextEditingController(text: '480'), + ]; + bool includeCursor = false, cropEnabled = false; + double frameRate = 30; + int bitrate = 0; + + @override + void initState() { + super.initState(); + capture = widget.controller ?? CaptureController(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) unawaited(capture.initialize()); + }); + } + + @override + void dispose() { + for (final field in cropFields) { + field.dispose(); + } + capture.dispose(); + super.dispose(); + } + + na.ScreenCaptureOptions? options() { + final values = cropFields + .map((field) => int.tryParse(field.text.trim())) + .toList(); + if (cropEnabled && + (values.any((value) => value == null) || + values[0]! < 0 || + values[1]! < 0 || + values[2]! <= 0 || + values[3]! <= 0)) { + capture.reportInvalidOptions( + 'Crop needs whole pixel values: X/Y ≥ 0 and Width/Height > 0.', + ); + return null; + } + return na.ScreenCaptureOptions( + includeCursor: includeCursor, + cropEnabled: cropEnabled, + crop: na.Rectangle( + x: (values[0] ?? 0).toDouble(), + y: (values[1] ?? 0).toDouble(), + width: (values[2] ?? 0).toDouble(), + height: (values[3] ?? 0).toDouble(), + ), + ); + } + + void takeScreenshot() { + final value = options(); + if (value != null) unawaited(capture.capture(value)); + } + + void startRecording() { + final value = options(); + if (value != null) { + unawaited( + capture.start( + na.ScreenRecordingOptions( + capture: value, + frameRate: frameRate, + videoBitrate: bitrate, + ), + ), + ); + } + } + + Widget action( + String label, + VoidCallback? callback, { + bool primary = false, + bool danger = false, + }) => Button( + variant: primary ? ButtonVariant.filled : ButtonVariant.normal, + tint: danger + ? ButtonTint.danger + : (primary ? ButtonTint.primary : ButtonTint.neutral), + onPressed: callback, + child: Text(label), + ); + + Widget section(String title, List children) => Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text(title, style: context.vars.titleMedium), + const SizedBox(height: 10), + ...children, + ], + ), + ); + + @override + Widget build(BuildContext context) => ListenableBuilder( + listenable: capture, + builder: (context, _) { + final vars = context.vars; + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + child: Row( + children: [ + Icon( + FluentIcons.record_24_regular, + color: vars.colorPrimary[600], + size: 28, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Screen Capture', style: vars.titleLarge), + Text( + 'Screenshots & silent recordings', + style: vars.muted, + ), + ], + ), + ), + Badge( + variant: BadgeVariant.tinted, + tint: capture.active ? BadgeTint.danger : BadgeTint.neutral, + child: Text( + capture.active + ? '${capture.recordingState.name} · ${capture.elapsed.toStringAsFixed(1)} s' + : capture.recordingState.name, + ), + ), + ], + ), + ), + const Divider(), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth < 880) { + return ListView( + padding: const EdgeInsets.all(20), + children: [ + controls(), + SizedBox(height: 450, child: preview()), + SizedBox(height: 220, child: results()), + ], + ); + } + return Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox( + width: 350, + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: controls(), + ), + ), + const VerticalDivider(), + Expanded( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + Expanded(child: preview()), + const SizedBox(height: 16), + SizedBox(height: 188, child: results()), + ], + ), + ), + ), + ], + ); + }, + ), + ), + const Divider(), + Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + color: capture.error == null + ? vars.colorSurfaceSunken + : vars.colorDanger[50], + child: Text( + capture.status, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: capture.error == null + ? vars.bodyMedium + : vars.bodyMedium.copyWith(color: vars.colorDanger[700]), + ), + ), + ], + ); + }, + ); + + Widget controls() { + final enabled = !capture.locked && capture.captureSupported; + final canCapture = enabled && capture.hasSource; + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (capture.ready && !capture.captureSupported) + const Padding( + padding: EdgeInsets.only(bottom: 16), + child: Text( + 'Capture requires macOS 14+ or Windows 10 2004+ with a supported desktop session.', + ), + ), + section('1 Choose a source', [ + action( + 'System picker…', + enabled && capture.pickerSupported ? capture.selectSource : null, + primary: true, + ), + const SizedBox(height: 8), + Wrap( + spacing: 6, + runSpacing: 6, + children: [ + action('This window', enabled ? capture.selectThisWindow : null), + for (var i = 0; i < capture.displays.length; i++) + action( + 'Display ${i + 1}${capture.displays[i].isPrimary ? ' · primary' : ''}', + enabled + ? () => capture.selectDisplay(capture.displays[i]) + : null, + ), + ], + ), + const SizedBox(height: 10), + Text( + capture.sourceLabel, + style: context.vars.bodyMedium, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + if (capture.hasSource) + Align( + alignment: Alignment.centerLeft, + child: action( + 'Release source', + !capture.locked ? capture.releaseSource : null, + ), + ), + ]), + section('2 Capture options', [ + Wrap( + spacing: 6, + runSpacing: 6, + children: [ + OptionChip( + label: 'Include cursor', + selected: includeCursor, + onTap: !capture.locked + ? () => setState(() => includeCursor = !includeCursor) + : null, + ), + OptionChip( + label: 'Crop region', + selected: cropEnabled, + onTap: !capture.locked + ? () => setState(() => cropEnabled = !cropEnabled) + : null, + ), + ], + ), + if (cropEnabled) ...[ + const SizedBox(height: 10), + Row( + children: [ + for (var i = 0; i < 4; i++) + Expanded( + child: Padding( + padding: EdgeInsets.only(right: i == 3 ? 0 : 6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + ['X', 'Y', 'Width', 'Height'][i], + style: context.vars.muted, + ), + const SizedBox(height: 4), + TextField( + controller: cropFields[i], + enabled: !capture.locked, + size: WidgetSize.small, + ), + ], + ), + ), + ), + ], + ), + const SizedBox(height: 6), + Text( + 'Source pixels, not desktop coordinates. The region must fit inside the captured image.', + style: context.vars.muted, + ), + ], + const SizedBox(height: 12), + Wrap( + spacing: 6, + runSpacing: 6, + children: [ + for (final fps in [15.0, 30.0, 60.0]) + OptionChip( + label: '${fps.toInt()} fps', + selected: frameRate == fps, + onTap: !capture.locked + ? () => setState(() => frameRate = fps) + : null, + ), + ], + ), + const SizedBox(height: 6), + Wrap( + spacing: 6, + runSpacing: 6, + children: [ + for (final mbps in [0, 4, 8, 16]) + OptionChip( + label: mbps == 0 ? 'Auto bitrate' : '$mbps Mbps', + selected: bitrate == mbps * 1000000, + onTap: !capture.locked + ? () => setState(() => bitrate = mbps * 1000000) + : null, + ), + ], + ), + ]), + section('3 Capture & record', [ + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + action( + 'Take screenshot', + canCapture ? takeScreenshot : null, + primary: true, + ), + action( + 'Save PNG', + !capture.locked && capture.hasImage + ? capture.saveScreenshot + : null, + ), + ], + ), + const SizedBox(height: 10), + if (!capture.active) + action( + 'Start recording', + canCapture && capture.recordingSupported ? startRecording : null, + primary: true, + ) + else + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + action( + 'Stop & save', + capture.recordingState == na.ScreenRecordingState.recording + ? capture.stop + : null, + primary: true, + ), + action( + 'Discard', + capture.recordingState == na.ScreenRecordingState.starting || + capture.recordingState == + na.ScreenRecordingState.recording + ? capture.cancel + : null, + danger: true, + ), + ], + ), + const SizedBox(height: 8), + Text( + 'MP4 · H.264 · SDR · no audio. Wait for “saved” before opening a recording.', + style: context.vars.muted, + ), + ]), + section('Output folder', [ + Text( + capture.outputDirectory.isEmpty + ? 'Preparing…' + : capture.outputDirectory, + style: context.vars.mono, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + action( + 'Choose folder…', + !capture.locked ? capture.chooseOutputDirectory : null, + ), + action( + 'Open folder', + capture.outputDirectory.isNotEmpty + ? () => capture.openPath( + capture.outputDirectory, + directory: true, + ) + : null, + ), + ], + ), + const SizedBox(height: 8), + Text( + 'Starts in a temporary folder. Choose another folder to keep your captures.', + style: context.vars.muted, + ), + ]), + Text( + 'On macOS, allow Screen & System Audio Recording in System Settings if direct capture is denied. Reopen the app if macOS asks you to.', + style: context.vars.muted, + ), + ], + ); + } + + Widget preview() => Card( + variant: CardVariant.sunken, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Text('Screenshot preview', style: context.vars.titleMedium), + const Spacer(), + if (capture.hasImage) + Text( + '${capture.imageWidth} × ${capture.imageHeight} px', + style: context.vars.mono, + ), + ], + ), + const SizedBox(height: 14), + Expanded( + child: ClipRect( + child: capture.preview == null + ? Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + FluentIcons.image_24_regular, + size: 48, + color: context.vars.colorContentMuted, + ), + const SizedBox(height: 14), + const Text('Your next screenshot appears here'), + const SizedBox(height: 6), + Text( + 'Choose “This window” for a quick first capture.', + style: context.vars.muted, + ), + ], + ), + ) + : InteractiveViewer( + minScale: 0.5, + maxScale: 5, + child: Center( + child: Image.memory( + capture.preview!, + fit: BoxFit.contain, + gaplessPlayback: true, + errorBuilder: (_, _, _) => const Center( + child: Text( + 'Preview could not be decoded. You can still save the PNG.', + ), + ), + ), + ), + ), + ), + ), + const SizedBox(height: 10), + Text( + capture.preview == null ? 'PNG screenshots · physical pixels' : 'Scroll to zoom · drag to pan · this is a still image, not a live feed', + style: context.vars.muted, + ), + ], + ), + ); + + Widget results() => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'Saved captures (${capture.artifacts.length})', + style: context.vars.titleMedium, + ), + const SizedBox(height: 8), + if (capture.artifacts.isEmpty) + Text( + 'Saved PNGs and completed MP4s appear here.', + style: context.vars.muted, + ) + else + Expanded( + child: ListView.separated( + itemCount: capture.artifacts.length, + separatorBuilder: (_, _) => const Divider(), + itemBuilder: (context, index) { + final artifact = capture.artifacts[index]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 9), + child: Row( + children: [ + Icon( + artifact.isVideo + ? FluentIcons.video_24_regular + : FluentIcons.image_24_regular, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(artifact.name, overflow: TextOverflow.ellipsis), + Text( + artifact.isVideo + ? 'MP4 · ${artifact.duration!.toStringAsFixed(1)} s' + : 'PNG · ${artifact.width} × ${artifact.height} px', + style: context.vars.muted, + ), + ], + ), + ), + action( + artifact.isVideo ? 'Play' : 'Open', + () => capture.openPath(artifact.path), + ), + ], + ), + ); + }, + ), + ), + ], + ); +} diff --git a/examples/flutter_screen_capture_example/macos/.gitignore b/examples/flutter_screen_capture_example/macos/.gitignore new file mode 100644 index 00000000..746adbb6 --- /dev/null +++ b/examples/flutter_screen_capture_example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/examples/flutter_screen_capture_example/macos/Flutter/Flutter-Debug.xcconfig b/examples/flutter_screen_capture_example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 00000000..c2efd0b6 --- /dev/null +++ b/examples/flutter_screen_capture_example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/examples/flutter_screen_capture_example/macos/Flutter/Flutter-Release.xcconfig b/examples/flutter_screen_capture_example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 00000000..c2efd0b6 --- /dev/null +++ b/examples/flutter_screen_capture_example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/examples/flutter_screen_capture_example/macos/Flutter/GeneratedPluginRegistrant.swift b/examples/flutter_screen_capture_example/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 00000000..14b5f7c7 --- /dev/null +++ b/examples/flutter_screen_capture_example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,12 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import file_selector_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) +} diff --git a/examples/flutter_screen_capture_example/macos/Runner.xcodeproj/project.pbxproj b/examples/flutter_screen_capture_example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..c1e9bf0b --- /dev/null +++ b/examples/flutter_screen_capture_example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,729 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* screen_capture_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "screen_capture_example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* screen_capture_example.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* screen_capture_example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.libnativeapi.screenCaptureExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/screen_capture_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/screen_capture_example"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.libnativeapi.screenCaptureExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/screen_capture_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/screen_capture_example"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.libnativeapi.screenCaptureExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/screen_capture_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/screen_capture_example"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/examples/flutter_screen_capture_example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/flutter_screen_capture_example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/examples/flutter_screen_capture_example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/examples/flutter_screen_capture_example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/examples/flutter_screen_capture_example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..2698d907 --- /dev/null +++ b/examples/flutter_screen_capture_example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/flutter_screen_capture_example/macos/Runner.xcworkspace/contents.xcworkspacedata b/examples/flutter_screen_capture_example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..1d526a16 --- /dev/null +++ b/examples/flutter_screen_capture_example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/examples/flutter_screen_capture_example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/flutter_screen_capture_example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/examples/flutter_screen_capture_example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/examples/flutter_screen_capture_example/macos/Runner/AppDelegate.swift b/examples/flutter_screen_capture_example/macos/Runner/AppDelegate.swift new file mode 100644 index 00000000..b3c17614 --- /dev/null +++ b/examples/flutter_screen_capture_example/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/examples/flutter_screen_capture_example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/flutter_screen_capture_example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..a2ec33f1 --- /dev/null +++ b/examples/flutter_screen_capture_example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/examples/flutter_screen_capture_example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/examples/flutter_screen_capture_example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 00000000..82b6f9d9 Binary files /dev/null and b/examples/flutter_screen_capture_example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/examples/flutter_screen_capture_example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/examples/flutter_screen_capture_example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 00000000..13b35eba Binary files /dev/null and b/examples/flutter_screen_capture_example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/examples/flutter_screen_capture_example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/examples/flutter_screen_capture_example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 00000000..0a3f5fa4 Binary files /dev/null and b/examples/flutter_screen_capture_example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/examples/flutter_screen_capture_example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/examples/flutter_screen_capture_example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 00000000..bdb57226 Binary files /dev/null and b/examples/flutter_screen_capture_example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/examples/flutter_screen_capture_example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/examples/flutter_screen_capture_example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 00000000..f083318e Binary files /dev/null and b/examples/flutter_screen_capture_example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/examples/flutter_screen_capture_example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/examples/flutter_screen_capture_example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 00000000..326c0e72 Binary files /dev/null and b/examples/flutter_screen_capture_example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/examples/flutter_screen_capture_example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/examples/flutter_screen_capture_example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 00000000..2f1632cf Binary files /dev/null and b/examples/flutter_screen_capture_example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/examples/flutter_screen_capture_example/macos/Runner/Base.lproj/MainMenu.xib b/examples/flutter_screen_capture_example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 00000000..80e867a4 --- /dev/null +++ b/examples/flutter_screen_capture_example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/flutter_screen_capture_example/macos/Runner/Configs/AppInfo.xcconfig b/examples/flutter_screen_capture_example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 00000000..ff04538a --- /dev/null +++ b/examples/flutter_screen_capture_example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = screen_capture_example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = dev.libnativeapi.screenCaptureExample + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 dev.libnativeapi. All rights reserved. diff --git a/examples/flutter_screen_capture_example/macos/Runner/Configs/Debug.xcconfig b/examples/flutter_screen_capture_example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 00000000..36b0fd94 --- /dev/null +++ b/examples/flutter_screen_capture_example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/examples/flutter_screen_capture_example/macos/Runner/Configs/Release.xcconfig b/examples/flutter_screen_capture_example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 00000000..dff4f495 --- /dev/null +++ b/examples/flutter_screen_capture_example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/examples/flutter_screen_capture_example/macos/Runner/Configs/Warnings.xcconfig b/examples/flutter_screen_capture_example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 00000000..42bcbf47 --- /dev/null +++ b/examples/flutter_screen_capture_example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/examples/flutter_screen_capture_example/macos/Runner/DebugProfile.entitlements b/examples/flutter_screen_capture_example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 00000000..256eeef5 --- /dev/null +++ b/examples/flutter_screen_capture_example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.files.user-selected.read-write + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/examples/flutter_screen_capture_example/macos/Runner/Info.plist b/examples/flutter_screen_capture_example/macos/Runner/Info.plist new file mode 100644 index 00000000..4789daa6 --- /dev/null +++ b/examples/flutter_screen_capture_example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/examples/flutter_screen_capture_example/macos/Runner/MainFlutterWindow.swift b/examples/flutter_screen_capture_example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 00000000..3cc05eb2 --- /dev/null +++ b/examples/flutter_screen_capture_example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/examples/flutter_screen_capture_example/macos/Runner/Release.entitlements b/examples/flutter_screen_capture_example/macos/Runner/Release.entitlements new file mode 100644 index 00000000..87e7e4cd --- /dev/null +++ b/examples/flutter_screen_capture_example/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.files.user-selected.read-write + + com.apple.security.app-sandbox + + + diff --git a/examples/flutter_screen_capture_example/macos/RunnerTests/RunnerTests.swift b/examples/flutter_screen_capture_example/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..61f3bd1f --- /dev/null +++ b/examples/flutter_screen_capture_example/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/examples/flutter_screen_capture_example/pubspec.yaml b/examples/flutter_screen_capture_example/pubspec.yaml new file mode 100644 index 00000000..200c5645 --- /dev/null +++ b/examples/flutter_screen_capture_example/pubspec.yaml @@ -0,0 +1,29 @@ +name: screen_capture_example +description: Screenshots and silent screen recordings using nativeapi on macOS and Windows. +publish_to: none +version: 1.0.0+1 +resolution: workspace + +environment: + sdk: ^3.13.0 + +dependencies: + flutter: + sdk: flutter + dazzui_host: + path: ../flutter_dazzui_host + nativeapi: + path: ../../bindings/dart/nativeapi + nativeapi_flutter: + path: ../../bindings/dart/nativeapi_flutter + file_selector: ^1.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + flutter_lints: ^5.0.0 + +flutter: + uses-material-design: false diff --git a/examples/flutter_screen_capture_example/windows/.gitignore b/examples/flutter_screen_capture_example/windows/.gitignore new file mode 100644 index 00000000..d492d0d9 --- /dev/null +++ b/examples/flutter_screen_capture_example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/examples/flutter_screen_capture_example/windows/CMakeLists.txt b/examples/flutter_screen_capture_example/windows/CMakeLists.txt new file mode 100644 index 00000000..b2d78695 --- /dev/null +++ b/examples/flutter_screen_capture_example/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(screen_capture_example LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "screen_capture_example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/examples/flutter_screen_capture_example/windows/flutter/CMakeLists.txt b/examples/flutter_screen_capture_example/windows/flutter/CMakeLists.txt new file mode 100644 index 00000000..903f4899 --- /dev/null +++ b/examples/flutter_screen_capture_example/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/examples/flutter_screen_capture_example/windows/flutter/generated_plugin_registrant.cc b/examples/flutter_screen_capture_example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..77ab7a09 --- /dev/null +++ b/examples/flutter_screen_capture_example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); +} diff --git a/examples/flutter_screen_capture_example/windows/flutter/generated_plugin_registrant.h b/examples/flutter_screen_capture_example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..dc139d85 --- /dev/null +++ b/examples/flutter_screen_capture_example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/examples/flutter_screen_capture_example/windows/flutter/generated_plugins.cmake b/examples/flutter_screen_capture_example/windows/flutter/generated_plugins.cmake new file mode 100644 index 00000000..a423a024 --- /dev/null +++ b/examples/flutter_screen_capture_example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/examples/flutter_screen_capture_example/windows/runner/CMakeLists.txt b/examples/flutter_screen_capture_example/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..394917c0 --- /dev/null +++ b/examples/flutter_screen_capture_example/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/examples/flutter_screen_capture_example/windows/runner/Runner.rc b/examples/flutter_screen_capture_example/windows/runner/Runner.rc new file mode 100644 index 00000000..91ec6ef4 --- /dev/null +++ b/examples/flutter_screen_capture_example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "dev.libnativeapi" "\0" + VALUE "FileDescription", "screen_capture_example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "screen_capture_example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 dev.libnativeapi. All rights reserved." "\0" + VALUE "OriginalFilename", "screen_capture_example.exe" "\0" + VALUE "ProductName", "screen_capture_example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/examples/flutter_screen_capture_example/windows/runner/flutter_window.cpp b/examples/flutter_screen_capture_example/windows/runner/flutter_window.cpp new file mode 100644 index 00000000..955ee303 --- /dev/null +++ b/examples/flutter_screen_capture_example/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/examples/flutter_screen_capture_example/windows/runner/flutter_window.h b/examples/flutter_screen_capture_example/windows/runner/flutter_window.h new file mode 100644 index 00000000..6da0652f --- /dev/null +++ b/examples/flutter_screen_capture_example/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/examples/flutter_screen_capture_example/windows/runner/main.cpp b/examples/flutter_screen_capture_example/windows/runner/main.cpp new file mode 100644 index 00000000..ba6ab093 --- /dev/null +++ b/examples/flutter_screen_capture_example/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"screen_capture_example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/examples/flutter_screen_capture_example/windows/runner/resource.h b/examples/flutter_screen_capture_example/windows/runner/resource.h new file mode 100644 index 00000000..66a65d1e --- /dev/null +++ b/examples/flutter_screen_capture_example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/examples/flutter_screen_capture_example/windows/runner/resources/app_icon.ico b/examples/flutter_screen_capture_example/windows/runner/resources/app_icon.ico new file mode 100644 index 00000000..c04e20ca Binary files /dev/null and b/examples/flutter_screen_capture_example/windows/runner/resources/app_icon.ico differ diff --git a/examples/flutter_screen_capture_example/windows/runner/runner.exe.manifest b/examples/flutter_screen_capture_example/windows/runner/runner.exe.manifest new file mode 100644 index 00000000..153653e8 --- /dev/null +++ b/examples/flutter_screen_capture_example/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/examples/flutter_screen_capture_example/windows/runner/utils.cpp b/examples/flutter_screen_capture_example/windows/runner/utils.cpp new file mode 100644 index 00000000..3cb71466 --- /dev/null +++ b/examples/flutter_screen_capture_example/windows/runner/utils.cpp @@ -0,0 +1,69 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + // First, find the length of the string with a safe upper bound (CWE-126). + // UNICODE_STRING_MAX_CHARS (32767) is the maximum length of a UNICODE_STRING. + int input_length = static_cast(wcsnlen(utf16_string, UNICODE_STRING_MAX_CHARS)); + // Now use that bounded length to determine the required buffer size. + // When an explicit length is passed, WideCharToMultiByte does not include + // the null terminator in its returned size. + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || static_cast(target_length) > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/examples/flutter_screen_capture_example/windows/runner/utils.h b/examples/flutter_screen_capture_example/windows/runner/utils.h new file mode 100644 index 00000000..3879d547 --- /dev/null +++ b/examples/flutter_screen_capture_example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/examples/flutter_screen_capture_example/windows/runner/win32_window.cpp b/examples/flutter_screen_capture_example/windows/runner/win32_window.cpp new file mode 100644 index 00000000..60608d0f --- /dev/null +++ b/examples/flutter_screen_capture_example/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/examples/flutter_screen_capture_example/windows/runner/win32_window.h b/examples/flutter_screen_capture_example/windows/runner/win32_window.h new file mode 100644 index 00000000..e901dde6 --- /dev/null +++ b/examples/flutter_screen_capture_example/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/pubspec.yaml b/pubspec.yaml index 1e23734b..45e454e3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,7 @@ workspace: - examples/flutter_display_example - examples/flutter_menu_example - examples/flutter_multiple_window_example + - examples/flutter_screen_capture_example - examples/flutter_storage_example - examples/flutter_tray_icon_example - examples/flutter_url_opener_example diff --git a/specs/README.md b/specs/README.md index f69c2c18..7693100f 100644 --- a/specs/README.md +++ b/specs/README.md @@ -17,6 +17,7 @@ | [c-abi.md](c-abi.md) | C ABI 长什么样、由谁产出、类型怎么过桥 | 生成管线已实施 | | [handle-ownership.md](handle-ownership.md) | C ABI 句柄的所有权与失效语义 | 已决策,实施中 | | [view.md](view.md) | 怎么在窗口里放原生控件(View)并响应它:类型、所有权、布局、事件 | 已实施 | +| [screen-capture.md](screen-capture.md) | 截图与录屏:采集源、异步授权、裁剪、录制生命周期与文件输出 | Windows/macOS 实施与验证中 | 新增一个跨平台模块,按顺序读 architecture → object-model → api-style → platform-seam; 要过 C ABI 再读 c-abi + handle-ownership。 diff --git a/specs/screen-capture.md b/specs/screen-capture.md new file mode 100644 index 00000000..8c866465 --- /dev/null +++ b/specs/screen-capture.md @@ -0,0 +1,628 @@ +# 屏幕截图与录屏 API 设计 + +> 状态:Windows/macOS 实施与验证中;截图入口命名已确定为 `ScreenCapturer` +> 适用范围:拟新增的 core 公共接口、六个平台接缝、C ABI 和六种语言绑定 +> 核实基准:2026-10-28,对照当前工作区 API 风格、对象模型、事件与绑定所有权规范 + +截图与录屏共用 `CaptureSource` 和 `ScreenCaptureOptions`。`ScreenCapturer` 执行 +一次截图并返回现有的 `Image`;`ScreenRecorder` 管理一次录制并输出视频文件。 +采集目标、授权会话、录制状态各自有明确的生命周期,不向调用方暴露平台原生句柄。 + +本文定义接口和行为契约,实施状态不能替代逐项验收。代码块用于说明公共设计,完整 +声明与英文 Doxygen 以公共头文件为准;平台可用性说明必须与实际验证结果一致。 +通用规则服从 [API 风格](api-style.md)、[对象模型](object-model.md)、 +[事件系统](event-system.md)、[C ABI](c-abi.md) 和 [句柄所有权](handle-ownership.md)。 + +## 1. 范围与类型分工 + +第一版覆盖单显示器、单窗口和单个源内矩形区域的截图与无音轨录屏。截图复用 +`Image::SaveToFile` 等现有图片能力;录屏固定为 MP4 容器、H.264 视频、SDR 输出。 +当前实施范围是 Windows/macOS;Linux 与移动平台保留相同符号并明确返回不支持。 + +| 类型 | 职责 | 对象模型 | +|---|---|---| +| `CaptureSource` | 保存一个目标及其可选授权会话 | 无集合 ID 的身份对象,`shared_ptr` | +| `ScreenCapturer` | 能力探测、异步源选择、异步截图 | 不可实例化的静态入口,无句柄 | +| `ScreenRecorder` | 一次录制的状态、停止、取消与结果事件 | 无集合 ID 的身份对象,`shared_ptr` | +| `CaptureSourceSelectionOptions` | 系统选择器的允许目标及父窗口 | 可拷贝值结构 | +| `ScreenCaptureOptions` | 光标及裁剪配置 | 可拷贝值结构 | +| `ScreenRecordingOptions` | 采集配置、帧率及码率 | 可拷贝值结构 | +| `ScreenRecordingEvent` 及派生类 | 录制状态通知和终结结果 | 可拷贝事件值对象 | + +不新增 manager、公共资源 ID 或全局录制单例;录制事件直接由对应 recorder 发出。 +`ScreenCapturer` 与 `ScreenRecorder` 都表示执行者;`ScreenCaptureOptions` 保留操作名, +不改成 `ScreenCapturerOptions`。 + +`ScreenCapturer` 不保存可变配置或“最近一张截图”,因此采用静态方法,每次请求显式 +传入 source 和 options。`ScreenRecorder` 则持有一次录制的持续状态,必须实例化。 +执行者命名不要求两者具有相同的构造方式。 + +第一版不包含跨屏拼接、多个源合成、窗口枚举与缩略图列表、区域选择遮罩 UI、标注、 +滚动截图、暂停/恢复、音频、HDR 文件输出、实时帧回调、直播及持久化授权令牌。 +这些能力的后续接口见 §12,不能先加入没有实现语义的配置字段。 + +## 2. 采集源 + +拟放在 `core/src/capture_source.h`。以下及后续声明均位于 `namespace nativeapi`。 +标准库包含、前向声明和大部分文档注释省略。 + +```cpp +enum class CaptureSourceType { + None, // No usable target is known. + Display, // A display target. + Window, // A window target. + Unknown, // A selected target whose kind the backend cannot identify. +}; + +class CaptureSource { + public: + static std::shared_ptr FromDisplay( + std::shared_ptr display); + static std::shared_ptr FromWindow( + std::shared_ptr window); + + ~CaptureSource(); + CaptureSource(const CaptureSource&) = delete; + CaptureSource& operator=(const CaptureSource&) = delete; + CaptureSource(CaptureSource&&) = delete; + CaptureSource& operator=(CaptureSource&&) = delete; + + CaptureSourceType GetType() const; + std::string GetName() const; + + private: + CaptureSource(); + friend class ScreenCapturer; + friend class ScreenRecorder; + class Impl; + std::unique_ptr pimpl_; +}; +``` + +### 2.1 创建与目标身份 + +- `FromDisplay` / `FromWindow` 同步创建目标描述,不请求权限、不弹 UI、不开始采集。 + 空参数、已知失效目标或当前后端不支持直接指定该类型时返回 `nullptr`。 +- `FromWindow` 接收已有的 `Window` 对象。当前 `WindowManager` 管理本应用窗口; + 捕获其他应用窗口应使用 §3 的 `SelectSource`,不能把外部窗口登记到本应用 manager。 +- `SelectSource` 的结果可以只持有平台授权对象或 portal 会话,无须能反查成 + `Display` / `Window`。因此不提供强制返回这两种对象的 getter。 +- 每个 `CaptureSource` 表示一次采集目标绑定。分别创建的源可以指向同一窗口,但不 + 承诺共享授权或实例去重;它们不是 manager 对原生窗口的重复查询。 +- `GetType` 返回绑定的目标类型;目标已知失效时为 `None`。`GetName` 活读当前可取得 + 的显示名称,名称不可得或目标失效时返回空字符串。名称不可作为稳定标识。 +- 系统已返回目标但没有可靠类型信息时,`GetType` 返回 `Unknown`,不猜测为窗口或 + 显示器。它与表示已知失效的 `None` 不同;两者都不是权限查询,实际采集仍可能失败。 +- 目标消失不会使 C++ 对象或 C 句柄悬垂。后续操作失败,显示器重新连接也不自动把旧 + source 绑定到新显示器;名称相同不足以认定为同一个目标。 + +### 2.2 授权、占用与释放 + +持有 source 不等于永久获准采集。直接指定的源在真正采集时仍可能需要系统授权; +通过选择器取得的源也可能被系统撤销。不能序列化 source 或把原生授权令牌公开给绑定。 + +同一个 source 在第一版只允许一个在途截图或一个活动录制。截图交付完成前或 recorder +处于 `Starting` / `Recording` / `Stopping` 时,第二个消费者被拒绝,不排队、不抢占。 +不同 source 的并发由后端决定,资源不足时按正常失败处理。 + +在途截图和活动 recorder 保留 source 的强引用。调用方可以释放自己那份引用,而不 +打断已受理的操作。终结后消费者释放 source;source 最后一份引用释放时关闭它所持有 +的授权会话和原生资源。单纯持有已选 source 在部分系统上就可能保留共享指示器或 +portal 流;不再使用时应释放,不应存入长期全局缓存。 + +`Stop` / `Cancel` 结束本次录制的数据消费和编码,不撤销调用方仍持有的 source 授权。 +不同平台对同一 source 的顺序复用能力不同:仅在既有目标和授权仍可使用时复用; +需要重新选源时操作失败,不静默选择替代目标。第一版不承诺跨会话复用。 + +## 3. 源选择与截图 + +拟放在 `core/src/screen_capturer.h`,包含 `capture_source.h` 和几何值类型。 + +```cpp +struct CaptureSourceSelectionOptions { + bool allow_displays = true; + bool allow_windows = true; + std::shared_ptr parent_window; +}; + +struct ScreenCaptureOptions { + bool include_cursor = false; + bool crop_enabled = false; + Rectangle crop = {0, 0, 0, 0}; +}; + +class ScreenCapturer { + public: + static bool IsSupported(); + static bool IsSourceSelectionSupported(); + static bool IsDirectCaptureSupported(CaptureSourceType type); + + static void SelectSource( + const CaptureSourceSelectionOptions& options, + std::function)> callback); + + static void Capture( + std::shared_ptr source, + const ScreenCaptureOptions& options, + std::function)> callback); + + private: + ScreenCapturer() = delete; +}; +``` + +### 3.1 能力查询 + +能力查询无 UI、无权限申请、无实际采集。它们反映当前后端和宿主可用性,不保证权限、 +目标存活、文件可写或之后一定成功。 + +| 查询 | `true` 的含义 | +|---|---| +| `ScreenCapturer::IsSupported()` | 当前存在至少一种可用截图路径 | +| `IsSourceSelectionSupported()` | 当前可发起交互选源,允许类型仍受后端限制 | +| `IsDirectCaptureSupported(type)` | 当前支持按已知对象直接指定该类型;`None` / `Unknown` 恒为 false | +| `ScreenRecorder::IsSupported()` | 当前具备至少一种采集路径及本规范要求的编码、封装路径 | + +模块可用不等于每种参数组合可用。请求的光标、目标类型、码率等无法兑现时必须失败, +不能把窗口截屏变成整个桌面截屏、无光标变成带光标、录屏变成单张图片。 + +### 3.2 选择器契约 + +选择器只允许返回一个源。两个 `allow_*` 都为 false 属于无效参数。允许类型是可选范围, +不是要求平台同时具备两类源;实际展示范围为允许类型与后端能力的交集。交集为空失败。 +系统返回允许范围之外的类型时失败,不能扩大采集范围。 + +若后端无法限制选择范围,也无法可靠识别所选类型,则只接受两个 `allow_*` 都为 true +的请求,成功时 source 类型为 `Unknown`。只允许一种类型的请求在打开 UI 前失败, +不能忽略过滤条件。`Unknown` 仅表示类型信息缺失,不允许选中第一版范围外的其他源。 + +`parent_window` 为选源 UI 的父窗口,不是被采集的窗口。空值时优先使用当前可用宿主 +窗口;后端必须提供父窗口而无法解析时失败。父窗口由请求持有到结束;原生窗口在 +请求期间关闭时使请求失败,并清理或忽略迟到的系统结果。 + +进程内最多一个在途 `SelectSource`。重复请求立即走失败完成路径,不打断已有选择器。 +该静态接口没有程序化取消方法;用户可在系统 UI 中取消。请求必须由用户操作发起, +不因能力探测、对象构造或后台轮询自动弹出选择器。 + +| 结果 | 回调 | +|---|---| +| 选择成功 | `true, source` | +| 用户正常取消选择器 | `true, nullptr` | +| 参数无效、功能不支持、系统拒绝访问或选择器失败 | `false, nullptr` | + +只有系统明确报告正常取消时才返回 `true, nullptr`;无法区分的拒绝按失败处理。 +用户取消不是异常,绑定的异步便利接口应保留可空结果。 + +### 3.3 截图契约 + +`Capture` 获取调用受理后的一个新有效帧,不返回上次调用的缓存。成功为 +`true, image`;所有失败为 `false, nullptr`。空 source、源被占用、授权拒绝、目标消失、 +不支持的选项、无效裁剪、获取帧或图片转换失败均不返回部分结果。 + +返回的 `Image` 持有独立像素内容。源关闭、录制结束或底层缓冲池复用不影响已交付图片。 +截图本身不保存文件,也不隐式写剪贴板;调用方组合现有 `Image` / `Clipboard` API。 + +不增加 `GetLastError`、C++ 异常或模块专属的结果包装。更细的机器可读失败原因需要先 +解决 [API 风格 §4](api-style.md#4-失败怎么表达) 的统一错误模型;当前 bool 契约无法 +区分截图授权失败与其他截图错误,这一限制是明确的。 + +## 4. 坐标、画面与格式 + +### 4.1 裁剪 + +`crop` 的单位是**采集源图像的物理像素**,原点在该图像左上角,x 向右、y 向下。 +它描述图像区域,不是窗口位置或全局 screen coordinates,不能直接传入 +`Window::GetBounds()` 或显示器的逻辑坐标。 + +- `crop_enabled == false` 时完全忽略 `crop`;捕获整个源,包含当前光标策略。 +- 开启时四个分量必须有限且为整数值,x/y 非负,width/height 为正;右边界和下边界 + 必须在当前有效源图像内。尽管 `Rectangle` 字段是 double,也不接受小数或 NaN。 +- 不自动舍入、夹取或改成全屏。校验在获得实际帧尺寸后完成,溢出也按无效参数处理。 +- 先规范图像朝向,再裁剪,最后执行录制输出缩放或像素补齐。裁剪不以桌面全局原点为准。 +- 多 DPI 显示器分别作为独立源。应用需要从逻辑坐标换算时必须使用该源实际的映射, + 不允许假定全桌面共享一个缩放系数。 + +源为窗口时,采集原生后端提供的窗口画面,不用桌面矩形截取来冒充窗口采集。装饰、 +阴影、最小化窗口和被遮挡窗口的可用性依赖后端,实施时须在公共注释中逐平台注明。 +第一版不提供统一的“仅内容区”开关,也不承诺捕获系统禁止采集的内容。 + +### 4.2 光标与颜色 + +`include_cursor` 控制光标是否合入像素;默认 false。后端不能提供请求的模式时失败。 +录屏没有单独的光标轨,裁剪范围外的光标不出现在结果里。 + +第一版交付 SDR 图像/视频。HDR 输入必须经明确的 SDR 转换,不能直接截断高动态范围 +通道造成错误画面;后端不能正确转换时失败。具体色彩转换实现和质量验证见 §12。 + +### 4.3 大小变化 + +截图返回该次有效帧经裁剪后的实际像素尺寸,不额外放大或缩小。 + +录屏以第一个有效帧经裁剪后的尺寸固定编码画布。H.264 所需的偶数尺寸通过右侧/底部 +补黑像素获得,不裁掉边缘内容。后续整个源尺寸变化时保持输出画布尺寸,等比缩放并 +居中补黑边,不拉伸。启用裁剪时区域始终使用相同的源像素坐标;源缩小导致区域越界 +则失败,不悄悄移动裁剪区域。 + +## 5. 录屏接口与事件 + +拟放在 `core/src/screen_recorder.h`,引用共用的 `ScreenCaptureOptions`。 + +```cpp +enum class ScreenRecordingState { + Idle, + Starting, + Recording, + Stopping, + Completed, + Cancelled, + Failed, +}; + +struct ScreenRecordingOptions { + ScreenCaptureOptions capture; + double frame_rate = 30.0; + size_t video_bitrate = 0; // Bits per second; zero selects automatically. +}; + +class ScreenRecordingEvent : public Event { + public: + std::string GetTypeName() const override; +}; + +class ScreenRecordingStartedEvent : public ScreenRecordingEvent { + public: + std::string GetTypeName() const override; +}; + +class ScreenRecordingCompletedEvent : public ScreenRecordingEvent { + public: + ScreenRecordingCompletedEvent(const std::string& file_path, double duration); + std::string GetFilePath() const; + double GetDuration() const; + std::string GetTypeName() const override; + // Private immutable event payload omitted. +}; + +class ScreenRecordingCancelledEvent : public ScreenRecordingEvent { + public: + std::string GetTypeName() const override; +}; + +class ScreenRecordingFailedEvent : public ScreenRecordingEvent { + public: + explicit ScreenRecordingFailedEvent(const std::string& message); + std::string GetMessage() const; + std::string GetTypeName() const override; + // Private immutable event payload omitted. +}; + +class ScreenRecorder : public EventEmitter { + public: + static bool IsSupported(); + + ScreenRecorder(); + ~ScreenRecorder() override; + ScreenRecorder(const ScreenRecorder&) = delete; + ScreenRecorder& operator=(const ScreenRecorder&) = delete; + ScreenRecorder(ScreenRecorder&&) = delete; + ScreenRecorder& operator=(ScreenRecorder&&) = delete; + + bool Start(std::shared_ptr source, + const std::string& file_path, + const ScreenRecordingOptions& options); + bool Stop(); + bool Cancel(); + + ScreenRecordingState GetState() const; + bool IsActive() const; + double GetDuration() const; + std::string GetFilePath() const; + + private: + class Impl; + std::unique_ptr pimpl_; +}; +``` + +事件名称就是 `GetTypeName()` 返回的字符串。事件负载按值保存,不通过 recorder 活读 +来还原历史结果。事件归属于订阅的 recorder,不增加 `ScreenRecorderId`。 +失败诊断文本供日志和展示使用,允许平台差异,不能通过解析文本实现业务分支。 + +`frame_rate` 必须有限且在 `[0.1, 240]` 内,表示目标最大采样帧率;实际帧率受源刷新、编码和负载 +限制,不保证恒定帧率,不为静止画面强制复制每一帧。媒体时间戳仍覆盖静止时间。 +`video_bitrate` 为目标 bit/s,取值不超过 `UINT32_MAX`,0 交由后端选择, +不是最终文件平均码率的精确承诺。 +后端不能接受非零配置时失败,不静默忽略。 + +## 6. 状态机与动作语义 + +```mermaid +stateDiagram-v2 + [*] --> Idle + Idle --> Starting: Start accepted + Starting --> Recording: First frame accepted + Recording --> Stopping: Stop accepted + Starting --> Stopping: Cancel accepted + Recording --> Stopping: Cancel accepted + Stopping --> Completed: Stop finalized + Stopping --> Cancelled: Cancel cleaned up + Starting --> Failed: Failure + Recording --> Failed: Failure + Stopping --> Failed: Failure +``` + +`Stopping` 同时覆盖保存收尾和取消清理;内部记录进入原因,不新增对外的取消中状态。 + +| 操作 | 允许状态 | 同步返回与后续结果 | +|---|---|---| +| `Start` | `Idle` | true 后立即为 `Starting`,稍后开始或失败 | +| `Stop` | `Recording` | true 后立即为 `Stopping`,稍后完成或失败 | +| `Cancel` | `Starting` / `Recording` | true 后立即为 `Stopping`,稍后取消或失败 | +| 重复/不合法操作 | 其他状态 | false,不改变状态、不产生新的结果事件 | + +### 6.1 受理与失败 + +`Start` 同步检查当前状态、非空 source、源占用、基本选项和路径形式等已知条件。 +同步拒绝返回 false,对象保持 `Idle`,可修正参数重试,不发 `FailedEvent`。 +需要实际授权、拿到帧、打开文件或创建编码器才能知道的错误,在受理后通过 +`ScreenRecordingFailedEvent` 报告。 + +进入 `Recording` 的条件是首个有效视频帧已被编码管线接受,不是选择器关闭或权限 +申请通过。每次成功受理的录制最多一个 `StartedEvent`;在 `Starting` 取消或失败时没有 +开始事件。`Stop` 在 `Starting` 返回 false,此时终止请求应使用 `Cancel`。 + +一个 recorder 只执行一次成功受理的录制。到达 `Completed` / `Cancelled` / `Failed` +后不能重启;下一次录制新建对象。这避免迟到的回调混入下一次录制。 + +### 6.2 完成、取消与竞态 + +- `Stop` 停止接收新帧,排空已接收编码任务,封装并关闭文件,成功发布目标文件后才 + 进入 `Completed`。之后才可播放或上传最终文件。 +- `Cancel` 停止采集、丢弃待编码内容并清理本次临时输出;不保留半成品。 + 清理失败进入 `Failed`,诊断中说明残留,不虚报 `Cancelled`。 +- `Stopping` 不再接受 `Cancel`,也不接受第二次 `Stop`;避免提交文件和删除文件竞争。 +- 系统撤销授权、目标消失或平台采集失败都进入 `Failed`,不伪装成用户主动取消或 + 正常保存结束,也不自动切换目标。 +- 状态转换由共享逻辑串行裁决。若取消先被接受,后端因这次取消产生的“流关闭”属于 + 取消清理;独立的写入/清理错误仍可导致失败。已进入终态后忽略所有迟到结果。 + +对象和宿主运行时仍存活时,每次被接受的录制恰好提交一个终结事件:Completed、 +Cancelled、Failed 三选一。事件按发生顺序交付,先更新状态再通知,开始事件不会 +排在终结事件之后。回调读取到的是当前状态,可能已经比某个排队事件更晚。 +有无监听器都不影响采集、状态推进或文件收尾。 + +### 6.3 getter 与析构 + +- `IsActive()` 在 `Starting` / `Recording` / `Stopping` 为 true,其余为 false。 +- `GetDuration()` 单位秒,非负;尚无有效帧时为 0。活动期间为已接受媒体的时长, + 不含等待授权的时间;完成时等于最终媒体时长。取消/失败保留终止时已录制时长供 + 诊断,不表示存在可用文件。 +- `GetFilePath()` 在受理前为空,受理后返回请求的目标绝对路径,终态保留;只有 + `Completed` 保证该路径在完成时已发布可用输出。它从不返回临时文件路径。 +- 析构首先 `ShutdownEmitter()`,撤销异步生产者并停止消费;未完成录制按丢弃处理, + 不尝试靠析构自动保存。与正在进行的发布操作串行裁决:已经发布完成的文件不能 + 再删除,尚未发布的任务不得在析构后发布。 +- 析构后不派发结果事件,宿主/绑定运行时关闭也不承诺继续交付。清理只能保留内部 + 操作上下文,不能回调已销毁的对象;不能等待一个必须由当前 UI 线程执行的回调。 + +调用方必须持有 recorder 到显式停止/取消完成,不能依靠 GC finalizer 保存录屏。 + +## 7. 文件输出契约 + +`file_path` 必须是非空、无内嵌 NUL 的 UTF-8 绝对本地文件路径,扩展名为 `.mp4` +(忽略大小写);父目录必须存在且可写。第一版不创建目录、不接受 URI、不弹保存框。 +最终路径已存在时失败,包含符号链接等已有目录项,绝不覆盖调用方原有内容。 + +写入使用目标同目录中独占创建的临时文件。最终提交采用平台支持的不覆盖发布操作, +发布时再次保证目标不存在;不能用“先检查,再普通覆盖 rename”的竞态实现替代。 +后端/文件系统不能提供所需保证时失败。临时文件命名是内部细节,不能成为公共 API。 + +`CompletedEvent` 仅在编码、封装、文件关闭和最终发布全部成功后发出。没有有效帧不能 +发布空视频。成功不承诺断电后数据仍持久化;进程崩溃可能留下临时文件,后续恢复机制 +不属于第一版。正常失败或取消应清理本次临时文件,清理错误如实报告,不触碰已有文件。 + +## 8. 线程、回调与资源所有权 + +所有接受 options 的操作在调用时复制值配置,并保留所需共享对象。调用方在返回后修改 +原结构不影响请求。非空回调是异步方法的前置条件;空回调不启动操作,也不弹 UI。 + +第一版控制操作和 getter 在宿主 UI 线程调用,绑定负责切换到该线程,不宣称任意线程 +可并发操作 recorder。像素采集、转换、编码与 I/O 在内部工作线程执行;不在 UI 线程 +等待首帧或文件封装。内部帧队列必须有界,过载按时间戳丢帧,不能无限积压内存。 + +宿主 dispatcher 正常运行时,选择/截图完成与录制事件总在后续 UI 循环交付,不在 +发起调用的栈上重入;同一回调只执行一次。交付前释放本次 source 的占用,允许截图 +回调里发起下一次操作,保留必要强引用直到回调交付结束。 + +未初始化宿主或 dispatcher 明确不可用时,能力探测返回 false,`Start` 同步返回 false; +`SelectSource` / `Capture` 的非空回调在调用线程同步收到失败,不启动任何平台工作。 +这是异步交付的明确例外,与现有宿主初始化约束一致。宿主在处理中退出时撤销回调并 +清理资源,不向已销毁的语言运行时调用完成或 release 函数。 + +录制监听是观察已有操作,不通过 `StartEventListening` / `StopEventListening` 启停 +录制。高频帧不进入公共事件系统。source 和图片的回调负载使用现有交付 lease;绑定 +复制值并 retain 需要保留的句柄后再确认释放,不能把缓冲池地址交给异步消费者。 + +## 9. 使用示例 + +以下是接口实现后的 C++ 用法;路径由应用确定,例子只展示调用顺序。 + +```cpp +auto source = CaptureSource::FromDisplay( + DisplayManager::GetInstance().GetPrimary()); + +ScreenCaptureOptions options; +options.include_cursor = false; +ScreenCapturer::Capture(source, options, [](bool success, auto image) { + if (!success) return; + bool saved = image->SaveToFile("/tmp/screenshot.png"); + // Handle saved == false separately from capture failure. +}); +``` + +```cpp +// Keep recorder in application state until its terminal event. +auto recorder = std::make_shared(); +recorder->AddListener( + [](const ScreenRecordingCompletedEvent& event) { + auto path = event.GetFilePath(); + auto seconds = event.GetDuration(); + // The finalized file is now available. + }); +recorder->AddListener( + [](const ScreenRecordingFailedEvent& event) { + auto message = event.GetMessage(); + // Display or log the diagnostic; do not parse it as an error code. + }); + +CaptureSourceSelectionOptions selection; +ScreenCapturer::SelectSource(selection, [recorder](bool success, auto source) { + if (!success || !source) return; + ScreenRecordingOptions options; + options.capture.include_cursor = true; + bool accepted = recorder->Start(source, "/tmp/recording.mp4", options); + // Handle !accepted; source is retained internally only if accepted. +}); + +// Later, from a UI action after StartedEvent: +// recorder->Stop(); // Wait for CompletedEvent or FailedEvent. +// To discard while Starting or Recording: recorder->Cancel(). +``` + +## 10. 平台后端与可用性 + +下表区分本轮实施路径与后续候选后端,**不表示所有平台已通过验收**。运行时仍须探测 +实际能力、权限和编码器,公共方法在六个平台上具有相同签名。 + +| 平台 | 拟采用的路径 | 第一阶段边界 | +|---|---|---| +| macOS | macOS 14+ ScreenCaptureKit;AVAssetWriter H.264/MP4 | 实施中;授权与系统选源不能混同于永久许可 | +| Windows | Windows 10 2004+ Windows.Graphics.Capture;Media Foundation H.264/MP4 | 实施中;处理父 HWND、设备丢失及大小变化 | +| Linux / Wayland | 后续候选:ScreenCast Portal + PipeWire;独立编码/封装管线 | 本轮桩实现;未来系统选源不承诺直接指定 `Display` / `Window` | +| Linux / X11 | 后续候选:独立直接采集后端;可用时使用 portal 选源 | 本轮桩实现;窗口采集不能用桌面区域冒充 | +| Android | 第一阶段桩实现 | 探测 false、工厂 nullptr、动作失败;后续需宿主及 MediaProjection 会话设计 | +| iOS | 第一阶段桩实现 | 探测 false、工厂 nullptr、动作失败;后续按目标 OS/SDK 与应用场景设计 | +| OpenHarmony | 第一阶段桩实现 | 探测 false、工厂 nullptr、动作失败;后续核对公开 SDK 和宿主接缝 | + +Wayland 的 ScreenCast Portal 在启动会话时选源,返回 PipeWire 流;源的位置/大小元数据 +可能缺失,且不一定等于像素坐标。因此授权源不能强制映射为本库 Display,也不能依赖 +一个全局逻辑坐标矩形实现全部捕获。[ScreenCast Portal 文档](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.ScreenCast.html) + +Windows.Graphics.Capture 支持显示器和窗口的帧采集,帧缓冲及其底层 surface 有明确的 +生命周期;图片必须在归还缓冲前取得独立内容,不能跨语言保留借用地址。 +[Windows Screen Capture 文档](https://learn.microsoft.com/en-us/windows/apps/develop/media-authoring-processing/screen-capture) + +核对公开接口后,`GraphicsCapturePicker` 没有源类型过滤参数,`GraphicsCaptureItem` +公开的名称、大小与关闭事件也不能可靠反推出目标类型。因此本设计规定该 picker 路径 +使用 §3.2 的双类型允许规则,返回 `Unknown`;直接指定窗口或显示器时类型仍然已知。 +这是根据公开接口作出的设计取舍,不能以窗口标题或图像尺寸猜测类型。 +[GraphicsCapturePicker](https://learn.microsoft.com/en-us/uwp/api/windows.graphics.capture.graphicscapturepicker)、 +[GraphicsCaptureItem](https://learn.microsoft.com/en-us/uwp/api/windows.graphics.capture.graphicscaptureitem) + +macOS 以 ScreenCaptureKit 的屏幕采集能力作为候选后端,具体截图、选择器和输出 API +按部署版本确定。[Apple ScreenCaptureKit 文档](https://developer.apple.com/documentation/screencapturekit) + +Android 后续接入不能承诺复用一次授权来创建任意多个采集会话;MediaProjection 文档 +要求每个会话取得用户同意,并规定 token 的单次使用约束。 +[Android MediaProjection 文档](https://developer.android.com/media/grow/media-projection) + +## 11. C ABI、绑定与代码组织 + +实施时拟新增以下公共头: + +- `core/src/capture_source.h`:源类型和身份对象。 +- `core/src/screen_capturer.h`:选择选项、采集选项与静态入口。 +- `core/src/screen_recorder.h`:录制选项、状态、事件和 recorder。 + +状态机、选项校验、事件顺序、占用裁决及文件发布契约尽量放在共享实现。原生授权、 +帧资源和编码器状态留在私有 PIMPL / 平台接缝,公开头不包含平台类型、宏或内部入口。 +六个平台均须提供符号;未支持的平台是明确桩实现,不能以缺定义代替不支持。 + +`CaptureSource` 和 `ScreenRecorder` 追加 `IdTypeTag`,不分配公开资源 ID; +`ScreenCapturer` 是静态工具类,不创建实例句柄。三个新 options 都按值结构跨 C ABI。 +特别验证嵌套的 `ScreenRecordingOptions.capture`、父窗口共享字段、静态工具类及 +`shared_ptr` 回调的全部生成结果,不能仅凭头文件可解析就认为六个绑定均已导出。 + +将公共头加入 `API_HEADERS` 后通过根目录 `./codegen` 驱动 C ABI 和六种绑定,不能 +手写生成输出。`ScreenCapturer` 的 C 函数前缀应为 `native_screen_capturer_`,不保留 +另一个 `ScreenCapture` 入口或名称兼容别名,因为该 API 尚未发布。 + +绑定保留源/图片/recorder 的引用所有权,按各自线程模型交付回调。Dart、JS、Python、 +C# 可以沿用已有 Future / Promise / awaitable / Task 便利层:选源取消得到 null,截图 +失败进入通用失败路径。Rust / Go 首先保留回调形态。所有绑定都必须区分 Start 的 +“受理”和 StartedEvent,以及 Stop 的“受理”和 CompletedEvent;不能把 bool 直接 +包装成“录屏文件已经保存”的异步结果。 + +原设计阶段仅保存文档。后续 Windows/macOS 实施已启动,只有公共头、平台代码、 +生成器、六绑定与实际采集验收全部完成后,才能将本文状态改为已实施。 + +## 12. 未决事项与后续扩展 + +下列事项仍需在实现前或对应扩展启动时定案,不用隐含假设填补: + +1. **桌面编码依赖及版本。** 确定 macOS / Windows 的原生编码路径,Linux 采用 + GStreamer、FFmpeg 或其他后端的构建、分发与许可策略;确定最小 OS/SDK 和缺少编码器 + 时的探测。不能因截图可用就报告录屏可用。 +2. **颜色与可见区域。** 明确 SDR 色彩转换、H.264 色彩元数据、窗口阴影/装饰、受保护 + 内容及最小化/锁屏行为的平台注释。平台无法检测受保护内容时,不保证将黑帧识别为 + 权限错误;仅对可检测的中断作失败判定。 +3. **首帧与启动超时。** 系统选择器等待用户期间不设置隐藏超时。授权后迟迟无首帧、 + 编码初始化或封装停滞的超时阈值和内部取消机制须在后端落地时定案;不可无限挂起, + 也不能把静止画面正常无更新误判为故障。 +4. **源的显式关闭。** 第一版通过释放 source 结束其授权会话。若语言 finalizer 不及时 + 释放或需要复用源的 UI 体验要求显式关闭,再设计 `Close` / 状态查询;不能在没有 + 明确跨平台语义时添加 `IsAuthorized` 一类误导性 getter。 +5. **统一错误模型。** 第一版仅使用既有 bool/nullptr、失败回调和失败事件。若产品必须 + 区分权限、取消、编码器缺失等错误,需要先推进全库错误规范,再同步本接口。 +6. **暂停与音频。** 暂停需要排除暂停段的媒体时间轴;音频需要分别定义系统音频与 + 麦克风、授权、混音及音画同步,不能简单增加两个开关后宣称跨平台支持。 +7. **帧流和预览。** 单独设计帧对象、像素格式、时间戳、缓冲所有权、背压与绑定线程。 + 不通过每帧生成一个公共 `Image` 事件来扩充本接口。 +8. **移动平台及多源。** 移动端需验证宿主、前台服务/扩展与系统选源约束;多源拼接 + 需独立定义布局、各源 DPI 和音视频时钟。第一版的单源接口不承诺这些能力。 + +实施中待收尾:核对各语言 options 默认值(当前生成器部分绑定使用零值或要求显式 +字段,不能将其误认作 C++ 默认配置);补足选择器、源失效、长时间录制与异步绑定的 +端到端验证。这些属于本轮验收缺口,不因基础编译或单个窗口 smoke 通过而视为完成。 + +## 13. 实施与验收检查单 + +2026-10-28 已取得的验证证据(不替代以下完整检查单): + +- `core/tests/screen_capture_test.cpp` 在 macOS 和 Windows 通过共享契约测试,包含 + 取消与编码失败同时发生时保留独立错误、源独占、析构清理以及不覆盖发布。 +- `core/tests/screen_capture_macos_smoke.mm` 与 `screen_capture_windows_smoke.cpp` + 在真实桌面采集测试自身窗口,验证裁剪像素、独立图片、H.264 解码、窗口变大后的 + 新画面、固定输出尺寸和静止尾段时长;测试不发送合成输入。Windows 由 + `core/tests/run_capture_windows.ps1` 在交互会话中运行,不能用 session 0 结果替代。 +- Windows 后端在源尺寸变化时释放旧帧引用并重新创建采集池与会话,确保静止目标也 + 能交付新尺寸的完整帧;编码结束补入带时间戳的尾帧,避免 MFT 丢失静止尾段。 +- Rust 的采集对象携带框架链接依赖,原生编译目标与 Rust 部署版本一致。已检查 + 最终 Mach-O:不使用采集时无 ScreenCaptureKit load command;使用采集且部署 + 目标为 macOS 11 时为 `LC_LOAD_WEAK_DYLIB`。这证明链接方式,不等于旧系统运行测试。 +- 已通过代码生成一致性检查,以及 Rust、Dart、C#、JS、Python、Go 的基础编译或 + 分析;Rust/JS/Python/Go 另有无 UI 的真实 ABI 嵌套配置和录制拒绝测试。现有测试集 + 中的条件性跳过仍是未执行项,不能据此宣称所有绑定异步场景通过。 + +Flutter 示例 `examples/flutter_screen_capture_example` 已在 macOS 启动,完成系统 +选源、2560 × 1440 截图预览和短时 MP4 录制。预览按 Data URI 解析 `Image.ToBase64` +的返回值。2026-10-31 整理 PR 时,以最新 main 为基线重新通过 macOS 原生构建、 +共享契约测试、生成器测试及示例静态分析。Windows Flutter 示例仍未实机运行; +示例的 Open / Play 目前受 UrlOpener 仅支持 HTTP/HTTPS 限制,不能打开本地输出。 +以上事实不代表下列完整验收已完成。 + +- [ ] 公共命名使用 `ScreenCapturer` / `ScreenRecorder`,options 使用操作名。 +- [ ] 公共头符合 API 风格检查单,六平台说明、单位、失败和生命周期注释齐全。 +- [ ] source / recorder 禁拷贝移动、登记 handle tag;静态入口不产生无意义实例。 +- [ ] 选源覆盖成功、用户取消、拒绝、父窗口销毁、重复选择和迟到结果;验证类型过滤与 `Unknown`。 +- [ ] 截图覆盖独立图片所有权、无效/失效/占用源、无首帧和授权失败。 +- [ ] 裁剪覆盖边界、负数、小数、非有限数、溢出、多 DPI 与窗口调整大小。 +- [ ] 录制覆盖每个合法转换、重复动作、Starting 取消、Stopping 拒绝取消和终态唯一性。 +- [ ] 无监听器时仍能录制和完成;事件顺序、回调重入、析构和运行时关闭安全。 +- [ ] 目标不存在/已存在/竞争创建、无写入权限、磁盘满、封装失败、清理失败均验证。 +- [ ] Completed 后实际解码视频,检查画面、尺寸、SDR 颜色、时间轴及静止段时长。 +- [ ] 原生帧队列有界,长时间录制不会随帧数增长内存;事件不逐帧跨绑定。 +- [ ] 桌面后端在真实桌面验证;移动桩符号齐全,未验证的平台不标成已支持。 +- [ ] `./codegen` 无新 API 的 skipped,核对 C ABI、六绑定和 API reference 输出。 +- [ ] 绑定覆盖异步负载 lease/retain、线程切换、用户取消与热重启/运行时关闭。 +- [ ] 实施完成后更新本文状态、核实日期及剩余平台限制,不提前把设计目标标为已实施。 diff --git a/tools/codegen/bindings/src/api_reference.rs b/tools/codegen/bindings/src/api_reference.rs index b02387c3..0df315a9 100644 --- a/tools/codegen/bindings/src/api_reference.rs +++ b/tools/codegen/bindings/src/api_reference.rs @@ -215,7 +215,7 @@ fn module_items( members.push(member(symbols::listener(class), "listener", None, cpp)); } for (key, method) in symbols::class_methods(class).into_iter().zip(&class.methods) { - let kind = if method.is_static && class.is_instance() { + let kind = if method.is_static && !class.is_singleton() { "static" } else { "method" diff --git a/tools/codegen/bindings/src/csharp.rs b/tools/codegen/bindings/src/csharp.rs index 07f18d3a..bd370061 100644 --- a/tools/codegen/bindings/src/csharp.rs +++ b/tools/codegen/bindings/src/csharp.rs @@ -869,6 +869,7 @@ fn generate_singleton_class(ctx: &mut Ctx, api: &Api, class: &Class, prefix: &st let out = &mut ctx.public; writeln!(out, "{}", class_decl(api, class)).unwrap(); writeln!(out, "{{").unwrap(); + if !class.is_static() { writeln!( out, " /// The shared instance backed by the native singleton." @@ -883,6 +884,7 @@ fn generate_singleton_class(ctx: &mut Ctx, api: &Api, class: &Class, prefix: &st writeln!(out).unwrap(); writeln!(out, " private {}() {{ }}", class.name).unwrap(); writeln!(out).unwrap(); + } } for method in &class.methods { @@ -890,7 +892,7 @@ fn generate_singleton_class(ctx: &mut Ctx, api: &Api, class: &Class, prefix: &st } if class.name == "Clipboard" { - writeln!(ctx.public, " private static bool ValidText(string? value) {{\n if (value is null) return true;\n if (value.IndexOf((char)0) >= 0) return false;\n try {{ new System.Text.UTF8Encoding(false, true).GetByteCount(value); return true; }}\n catch (System.Text.EncoderFallbackException) {{ return false; }}\n }}").unwrap(); + writeln!(ctx.public, " private static bool ValidText(string? value)\n {{\n if (value is null) return true;\n if (value.IndexOf((char)0) >= 0) return false;\n try {{ new System.Text.UTF8Encoding(false, true).GetByteCount(value); return true; }}\n catch (System.Text.EncoderFallbackException) {{ return false; }}\n }}\n").unwrap(); } generate_async_methods(ctx, class); generate_listener(ctx, api, class, prefix); @@ -2010,7 +2012,7 @@ fn generate_async_methods(ctx: &mut Ctx, class: &Class) { declaration, }) = async_decl(method) { - writeln!(ctx.public, " {declaration} {{\n var completion = new System.Threading.Tasks.TaskCompletionSource<{ty}>(System.Threading.Tasks.TaskCreationOptions.RunContinuationsAsynchronously);\n {name}((success, value) => {{ if (success) completion.TrySetResult(value); else completion.TrySetException(new InvalidOperationException(\"Clipboard operation failed\")); }});\n return completion.Task;\n }}").unwrap(); + writeln!(ctx.public, " {declaration}\n {{\n var completion = new System.Threading.Tasks.TaskCompletionSource<{ty}>(System.Threading.Tasks.TaskCreationOptions.RunContinuationsAsynchronously);\n {name}((success, value) => {{ if (success) completion.TrySetResult(value); else completion.TrySetException(new InvalidOperationException(\"Clipboard operation failed\")); }});\n return completion.Task;\n }}\n").unwrap(); } } } @@ -2077,6 +2079,9 @@ fn event_variant_decl(group: &EventGroup, variant: &codegen_shared::ir::EventVar } fn class_decl(api: &Api, class: &Class) -> String { + if class.is_static() { + return format!("public static partial class {}", class.name); + } if !class.is_instance() { return format!("public sealed partial class {}", class.name); } @@ -2140,7 +2145,7 @@ fn method_decl(class: &Class, method: &Method) -> MethodDecl { let declaration = if property { format!("public {return_type} {name}") } else { - let keyword = if class.is_instance() && method.is_static { + let keyword = if !class.is_singleton() && method.is_static { "public static" } else { "public" diff --git a/tools/codegen/bindings/src/dart.rs b/tools/codegen/bindings/src/dart.rs index f77da541..de0bcb4c 100644 --- a/tools/codegen/bindings/src/dart.rs +++ b/tools/codegen/bindings/src/dart.rs @@ -1102,6 +1102,8 @@ fn render_dart_class(out: &mut String, api: &Api, header: &Header, class: &Class .unwrap(); writeln!(out, " }}").unwrap(); writeln!(out).unwrap(); + } else if class.is_static() { + writeln!(out, " {}._();", class.name).unwrap(); } else { // Singletons surface as `.instance` rather than static namespaces so // they can implement interfaces and be swapped in tests. @@ -1223,7 +1225,7 @@ fn dart_method_decl(class: &Class, method: &Method) -> (String, String) { } else { // Singleton methods are instance methods on `.instance`; only a static // method of a handle class stays static. - let prefix_kw = if class.is_instance() && method.is_static { + let prefix_kw = if !class.is_singleton() && method.is_static { " static " } else { " " diff --git a/tools/codegen/bindings/src/python.rs b/tools/codegen/bindings/src/python.rs index 6addf689..b95efe5a 100644 --- a/tools/codegen/bindings/src/python.rs +++ b/tools/codegen/bindings/src/python.rs @@ -947,7 +947,7 @@ fn render_struct(out: &mut String, module: &mut Module, item: &Struct) { // _from_c: copies a C value out; the caller frees it if it owns it. writeln!(out, " @classmethod").unwrap(); - writeln!(out, " def _from_c(cls, raw: {c_ty}) -> {}:", item.name).unwrap(); + write_indented(out, &def_decl_raw("_from_c", &["cls".into(), format!("raw: {c_ty}")], &item.name)); let values: Vec = item .fields .iter() @@ -1046,7 +1046,12 @@ fn struct_field_decl(module: &mut Module, field: &Field) -> String { other => annotation(module, other, Position::Field), }; let default = field_default(module, &field.ty); - format!("{}: {ty} = {default}", py_field_name(field)) + let line = format!("{}: {ty} = {default}", py_field_name(field)); + if line.len() + MEMBER_INDENT.len() <= 88 { + line + } else { + format!("{}: {ty} = (\n {default}\n)", py_field_name(field)) + } } fn struct_constant_name(constant: &str) -> String { @@ -1199,6 +1204,14 @@ fn event_value(module: &mut Module, field: &Field, base: &str) -> String { // --- classes --- fn render_class(out: &mut String, module: &mut Module, class: &Class) { + if class.is_static() { + writeln!(out, "{}", class_decl(class)).unwrap(); + writeln!(out, " def __init__(self) -> None:").unwrap(); + writeln!(out, " raise TypeError(\"{} contains only static methods\")", class.name).unwrap(); + render_methods(out, module, class); + writeln!(out, "\n").unwrap(); + return; + } match class.kind { ClassKind::Instance => render_instance_class(out, module, class), ClassKind::Singleton => render_singleton_class(out, module, class), @@ -1310,6 +1323,9 @@ fn property_setters<'a>(module: &mut Module, class: &'a Class) -> Vec<(&'a Metho /// `class Window(_rt.NativeObject):`, `class Application:`. fn class_decl(class: &Class) -> String { + if class.is_static() { + return format!("class {}:", class.name); + } match class.kind { ClassKind::Instance => { // A derived class inherits the base's handle plumbing (and diff --git a/tools/codegen/bindings/src/rust.rs b/tools/codegen/bindings/src/rust.rs index 53c07bd6..76d4ea30 100644 --- a/tools/codegen/bindings/src/rust.rs +++ b/tools/codegen/bindings/src/rust.rs @@ -126,6 +126,16 @@ fn render_rust_wrapper(api: &Api, header: &Header, origins: &TypeOrigins, prefix .into_iter() .filter_map(|name| origins.get(&name).map(|stem| (stem.clone(), name))) .collect(); + // Nested value structs keep their conversion buffers alive in RawOf. + for item in &header.structs { + for field in &item.fields { + if let TypeRef::Struct { name, .. } = &field.ty { + if let Some(module) = origins.get(name).filter(|module| *module != &header.stem) { + imports.push((module.clone(), format!("RawOf{name}"))); + } + } + } + } // Event handles use the non-owning Ref wrapper, which is a distinct Rust // type even though the IR names only the underlying C++ class. for group in &header.events { @@ -182,7 +192,7 @@ fn render_rust_wrapper(api: &Api, header: &Header, origins: &TypeOrigins, prefix } for item in &header.structs { - render_rust_struct(&mut out, item, prefix); + render_rust_struct(&mut out, api, item, prefix); } for group in &header.events { @@ -281,23 +291,15 @@ fn c_enum_variant_const(prefix: &str, item: &codegen_shared::ir::Enum, variant: codegen_shared::naming::c_enum_variant(prefix, item, variant) } -fn render_rust_struct(out: &mut String, item: &Struct, prefix: &str) { - // A callback field holds a closure, which is neither printable nor - // comparable, so those derives have to go when one is present. - let derives = if item - .fields - .iter() - .any(|field| matches!(field.ty, TypeRef::Object { .. })) - { - "#[derive(Debug)]" - } else if item.fields.iter().any(|field| is_callback(&field.ty)) { - "#[derive(Clone)]" - } else if struct_has_float_fields(item) { - "#[derive(Debug, Clone, PartialEq)]" - } else { - "#[derive(Debug, Clone, PartialEq, Eq)]" - }; - writeln!(out, "{derives}").unwrap(); +fn render_rust_struct(out: &mut String, api: &Api, item: &Struct, prefix: &str) { + // Trait support is transitive: a nested Rectangle is not Eq, and a + // nested handle/callback cannot gain traits its fields don't implement. + let traits = struct_traits(api, item); + let names: Vec<_> = [(1, "Debug"), (2, "Clone"), (4, "PartialEq"), (8, "Eq")] + .into_iter().filter_map(|(bit, name)| (traits & bit != 0).then_some(name)).collect(); + if !names.is_empty() { + writeln!(out, "#[derive({})]", names.join(", ")).unwrap(); + } writeln!(out, "{} {{", struct_decl(item)).unwrap(); for field in &item.fields { writeln!(out, " {},", struct_field_decl(field).1).unwrap(); @@ -471,10 +473,57 @@ fn owned_tuple_type(item: &Struct) -> String { parts.join(", ") } -fn struct_has_float_fields(item: &Struct) -> bool { - item.fields - .iter() - .any(|field| matches!(field.ty, TypeRef::Float { .. })) +fn struct_traits(api: &Api, item: &Struct) -> u8 { + item.fields.iter().fold(15, |traits, field| traits & type_traits(api, &field.ty)) +} + +fn type_traits(api: &Api, ty: &TypeRef) -> u8 { + match ty { + TypeRef::Float { .. } => 7, + TypeRef::Object { .. } => 1, + TypeRef::Callback { .. } => 2, + TypeRef::Optional { inner } => type_traits(api, inner), + TypeRef::Vector { element } => type_traits(api, element), + TypeRef::Struct { name, .. } => api.headers.iter() + .flat_map(|header| &header.structs).find(|item| item.name == *name) + .map_or(0, |item| struct_traits(api, item)), + _ => 15, + } +} + +#[cfg(test)] +mod struct_tests { + use super::*; + + #[test] + fn nested_cross_module_values_import_raw_storage_and_preserve_trait_limits() { + let rectangle = Struct { + name: "Rectangle".into(), qualified_name: "nativeapi::Rectangle".into(), + fields: vec![Field { name: "width".into(), ty: TypeRef::Float { name: "double".into() } }], + methods: vec![], constants: vec![], + }; + let capture = Struct { + name: "CaptureOptions".into(), qualified_name: "nativeapi::CaptureOptions".into(), + fields: vec![Field { name: "crop".into(), ty: TypeRef::Struct { + name: "Rectangle".into(), qualified_name: "nativeapi::Rectangle".into(), + } }], methods: vec![], constants: vec![], + }; + let header = |stem: &str, item| Header { + path: format!("{stem}.h").into(), stem: stem.into(), namespace: "nativeapi".into(), + enums: vec![], structs: vec![item], classes: vec![], aliases: vec![], events: vec![], + }; + let api = Api { headers: vec![header("geometry", rectangle), header("capture", capture)], + diagnostics: vec![], docs: Default::default() }; + let origins = codegen_shared::naming::type_origins(&api); + let text = render_rust_wrapper(&api, &api.headers[1], &origins, "native_"); + assert!(text.contains("RawOfRectangle")); + assert!(text.contains("use crate::geometry::{RawOfRectangle, Rectangle}")); + assert!(text.contains("#[derive(Debug, Clone, PartialEq)]")); + assert!(!text.contains("PartialEq, Eq")); + assert_eq!(type_traits(&api, &TypeRef::Object { + name: "Window".into(), qualified_name: "nativeapi::Window".into(), shared: true, + }), 1); + } } // --------------------------------------------------------------------------- diff --git a/tools/codegen/capi/src/generator.rs b/tools/codegen/capi/src/generator.rs index 62edfe89..ba8c8490 100644 --- a/tools/codegen/capi/src/generator.rs +++ b/tools/codegen/capi/src/generator.rs @@ -15,7 +15,7 @@ use codegen_shared::naming::{ c_params, c_release_user_data_param, c_remove_listener_symbol, c_return_type, c_self_param, c_to_cpp_fn, c_type_name, cpp_argument_expr, cpp_event_converter, cpp_event_releaser, cpp_to_c_fn, foreign_types, header_dependencies, is_callback, listed_classes, relative_include, - struct_has_owned_fields, TypeOrigins, COMMON_HEADER, LISTENER_ID_TYPE, RELEASE_USER_DATA_TYPE, + TypeOrigins, COMMON_HEADER, LISTENER_ID_TYPE, RELEASE_USER_DATA_TYPE, STRING_DUP_FN, STRING_FREE_FN, STRING_LIST_DUP_FN, STRING_LIST_FREE_FN, STRING_LIST_TYPE, STRING_MAP_DUP_FN, STRING_MAP_TYPE, USER_DATA_HEADER, }; @@ -925,7 +925,10 @@ fn render_struct_api_decl(out: &mut String, item: &Struct, prefix: &str) { writeln!(out).unwrap(); } - if struct_has_owned_fields(item) { + // Every value struct has a releaser, including PODs (a no-op). Nested + // structs and callback payloads can then compose cleanup without needing + // the definition of a struct owned by another header at the call site. + { writeln!(out, "/// Frees everything the struct owns.").unwrap(); writeln!(out, "FFI_PLUGIN_EXPORT").unwrap(); writeln!( @@ -2251,9 +2254,7 @@ fn render_struct_api(out: &mut String, item: &Struct, prefix: &str) { writeln!(out).unwrap(); } - if struct_has_owned_fields(item) { - render_c_free(out, item, prefix); - } + render_c_free(out, item, prefix); } fn render_c_free(out: &mut String, item: &Struct, prefix: &str) { @@ -2649,6 +2650,29 @@ mod tests { use super::*; use codegen_shared::ir::EnumVariant; + #[test] + fn nested_pod_cleanup_has_a_declared_noop_releaser() { + let rectangle = Struct { + name: "Rectangle".into(), qualified_name: "nativeapi::Rectangle".into(), + fields: vec![Field { name: "width".into(), ty: TypeRef::Float { name: "double".into() } }], + methods: vec![], constants: vec![], + }; + let options = Struct { + name: "CaptureOptions".into(), qualified_name: "nativeapi::CaptureOptions".into(), + fields: vec![Field { name: "crop".into(), ty: TypeRef::Struct { + name: "Rectangle".into(), qualified_name: "nativeapi::Rectangle".into(), + } }], methods: vec![], constants: vec![], + }; + let mut declaration = String::new(); + render_struct_api_decl(&mut declaration, &rectangle, "native_"); + assert!(declaration.contains("void native_rectangle_free(native_rectangle_t* value)")); + let mut implementation = String::new(); + render_struct_api(&mut implementation, &rectangle, "native_"); + render_struct_api(&mut implementation, &options, "native_"); + assert!(implementation.contains("void native_rectangle_free(native_rectangle_t* value)")); + assert!(implementation.contains("native_rectangle_free(&value->crop)")); + } + #[test] fn complex_callbacks_lease_copied_payload_and_preserve_empty_optional_strings() { let params = vec![ @@ -2706,7 +2730,7 @@ mod tests { methods: vec![], constants: vec![], }; - assert!(struct_has_owned_fields(&item)); + assert!(codegen_shared::naming::struct_has_owned_fields(&item)); let mut out = String::new(); render_c_struct_converter(&mut out, &item, "native_"); render_cpp_struct_converter(&mut out, &item, "native_"); diff --git a/tools/codegen/shared/src/ir/model.rs b/tools/codegen/shared/src/ir/model.rs index 01989c8d..53606119 100644 --- a/tools/codegen/shared/src/ir/model.rs +++ b/tools/codegen/shared/src/ir/model.rs @@ -193,7 +193,18 @@ impl Class { } pub fn is_instance(&self) -> bool { + matches!(self.kind, ClassKind::Instance) && !self.is_static() + } + + /// A non-constructible namespace of static operations has no native identity. + pub fn is_static(&self) -> bool { matches!(self.kind, ClassKind::Instance) + && self.constructors.is_empty() + && !self.methods.is_empty() + && self.methods.iter().all(|method| method.is_static) + && self.event.is_none() + && self.base.is_none() + && !self.native_object } } diff --git a/tools/codegen/shared/src/lib.rs b/tools/codegen/shared/src/lib.rs index 583df1e5..6188a5f4 100644 --- a/tools/codegen/shared/src/lib.rs +++ b/tools/codegen/shared/src/lib.rs @@ -32,6 +32,9 @@ pub const API_HEADERS: &[&str] = &[ "file_dialog.h", "notification_manager.h", "image.h", + "capture_source.h", + "screen_capturer.h", + "screen_recorder.h", "window_shape.h", "window_shadow.h", "view.h", diff --git a/website/content/api/capture_source.json b/website/content/api/capture_source.json new file mode 100644 index 00000000..a884cd71 --- /dev/null +++ b/website/content/api/capture_source.json @@ -0,0 +1,402 @@ +{ + "$comment": "AUTO-GENERATED. DO NOT EDIT. Generated by tools/codegen from the core headers; run ./codegen.", + "id": "capture_source", + "header": "capture_source.h", + "items": [ + { + "key": "CaptureSource", + "kind": "class", + "doc": { + "summary": "A capture target and its optional system authorization session.", + "details": "Sources are shared identity objects, not persistent permission tokens. A source accepts only one in-flight screenshot or recording. Destroying the last reference releases its authorization resources. A disappearing target is never replaced. Factories only describe targets; permission may be requested during capture. Obtain other applications' windows through ScreenCapturer::SelectSource().\n\n```cpp\nauto source = CaptureSource::FromDisplay(DisplayManager::GetInstance().GetPrimary());\n```", + "notes": [ + { + "kind": "note", + "text": "Platform availability:\n- macOS: ⚠️ ScreenCaptureKit on macOS 14+; system permission required\n- Windows: ⚠️ Windows Graphics Capture on Windows 10 2004+; desktop session required\n- Linux: ❌ Not implemented - Factories return nullptr\n- Android: ❌ Not implemented - Factories return nullptr\n- iOS: ❌ Not implemented - Factories return nullptr\n- OpenHarmony: ❌ Not implemented - Factories return nullptr" + } + ] + }, + "signatures": { + "cpp": { + "name": "CaptureSource", + "signature": "class CaptureSource" + }, + "csharp": { + "name": "CaptureSource", + "signature": "public sealed partial class CaptureSource : IDisposable" + }, + "dart": { + "name": "CaptureSource", + "signature": "class CaptureSource implements ffi.Finalizable" + }, + "go": { + "name": "CaptureSource", + "signature": "type CaptureSource struct" + }, + "js": { + "name": "CaptureSource", + "signature": "export class CaptureSource extends NativeObject" + }, + "python": { + "name": "CaptureSource", + "signature": "class CaptureSource(_rt.NativeObject):" + }, + "rust": { + "name": "CaptureSource", + "signature": "pub struct CaptureSource" + } + }, + "members": [ + { + "key": "CaptureSource::FromDisplay", + "kind": "static", + "doc": { + "summary": "Describes a display without prompting or starting capture.", + "params": [ + { + "name": "display", + "text": "Target display." + } + ], + "returns": "nullptr for null, unavailable or unsupported targets." + }, + "signatures": { + "cpp": { + "name": "FromDisplay", + "signature": "static std::shared_ptr FromDisplay(std::shared_ptr display)" + }, + "csharp": { + "name": "FromDisplay", + "signature": "public static CaptureSource? FromDisplay(Display? display)" + }, + "dart": { + "name": "fromDisplay", + "signature": "static CaptureSource? fromDisplay(Display? display)" + }, + "go": { + "name": "CaptureSourceFromDisplay", + "signature": "func CaptureSourceFromDisplay(display *Display) (*CaptureSource, error)" + }, + "js": { + "name": "fromDisplay", + "signature": "static fromDisplay(display: Display | null): CaptureSource | null" + }, + "python": { + "name": "from_display", + "signature": "@staticmethod\ndef from_display(display: _display.Display | None) -> CaptureSource | None:" + }, + "rust": { + "name": "from_display", + "signature": "pub fn from_display(display: Option<&Display>) -> Option" + } + } + }, + { + "key": "CaptureSource::FromWindow", + "kind": "static", + "doc": { + "summary": "Describes a window without prompting or starting capture.", + "params": [ + { + "name": "window", + "text": "Target window." + } + ], + "returns": "nullptr for null, unavailable or unsupported targets." + }, + "signatures": { + "cpp": { + "name": "FromWindow", + "signature": "static std::shared_ptr FromWindow(std::shared_ptr window)" + }, + "csharp": { + "name": "FromWindow", + "signature": "public static CaptureSource? FromWindow(Window? window)" + }, + "dart": { + "name": "fromWindow", + "signature": "static CaptureSource? fromWindow(Window? window)" + }, + "go": { + "name": "CaptureSourceFromWindow", + "signature": "func CaptureSourceFromWindow(window *Window) (*CaptureSource, error)" + }, + "js": { + "name": "fromWindow", + "signature": "static fromWindow(window: Window | null): CaptureSource | null" + }, + "python": { + "name": "from_window", + "signature": "@staticmethod\ndef from_window(window: _window.Window | None) -> CaptureSource | None:" + }, + "rust": { + "name": "from_window", + "signature": "pub fn from_window(window: Option<&Window>) -> Option" + } + } + }, + { + "key": "CaptureSource::GetType", + "kind": "method", + "doc": { + "summary": "Gets the target kind, or None when known to be unavailable." + }, + "signatures": { + "cpp": { + "name": "GetType", + "signature": "CaptureSourceType GetType() const" + }, + "csharp": { + "name": "Type", + "signature": "public CaptureSourceType Type" + }, + "dart": { + "name": "type", + "signature": "CaptureSourceType get type" + }, + "go": { + "name": "Type", + "signature": "func (cs *CaptureSource) Type() CaptureSourceType" + }, + "js": { + "name": "type", + "signature": "get type(): CaptureSourceType" + }, + "python": { + "name": "type", + "signature": "@property\ndef type(self) -> CaptureSourceType:" + }, + "rust": { + "name": "r#type", + "signature": "pub fn r#type(&self) -> CaptureSourceType" + } + } + }, + { + "key": "CaptureSource::GetName", + "kind": "method", + "doc": { + "summary": "Gets the current display name, or an empty string when unavailable." + }, + "signatures": { + "cpp": { + "name": "GetName", + "signature": "std::string GetName() const" + }, + "csharp": { + "name": "Name", + "signature": "public string? Name" + }, + "dart": { + "name": "name", + "signature": "String? get name" + }, + "go": { + "name": "Name", + "signature": "func (cs *CaptureSource) Name() string" + }, + "js": { + "name": "name", + "signature": "get name(): string" + }, + "python": { + "name": "name", + "signature": "@property\ndef name(self) -> str:" + }, + "rust": { + "name": "name", + "signature": "pub fn name(&self) -> Option" + } + } + } + ] + }, + { + "key": "CaptureSourceType", + "kind": "enum", + "doc": { + "summary": "The kind of content bound to a capture source." + }, + "signatures": { + "cpp": { + "name": "CaptureSourceType", + "signature": "enum class CaptureSourceType" + }, + "csharp": { + "name": "CaptureSourceType", + "signature": "public enum CaptureSourceType" + }, + "dart": { + "name": "CaptureSourceType", + "signature": "enum CaptureSourceType" + }, + "go": { + "name": "CaptureSourceType", + "signature": "type CaptureSourceType int32" + }, + "js": { + "name": "CaptureSourceType", + "signature": "export const CaptureSourceType =\nexport type CaptureSourceType = (typeof CaptureSourceType)[keyof typeof CaptureSourceType]" + }, + "python": { + "name": "CaptureSourceType", + "signature": "class CaptureSourceType(enum.IntEnum):" + }, + "rust": { + "name": "CaptureSourceType", + "signature": "pub enum CaptureSourceType" + } + }, + "members": [ + { + "key": "CaptureSourceType::None", + "kind": "variant", + "doc": { + "summary": "No live target." + }, + "signatures": { + "cpp": { + "name": "None", + "signature": "None = 0" + }, + "csharp": { + "name": "CaptureSourceType.None", + "signature": "None = 0" + }, + "dart": { + "name": "CaptureSourceType.none", + "signature": "none(0)" + }, + "go": { + "name": "CaptureSourceTypeNone", + "signature": "const CaptureSourceTypeNone CaptureSourceType = 0" + }, + "js": { + "name": "CaptureSourceType.None", + "signature": "None: 0" + }, + "python": { + "name": "NONE", + "signature": "NONE = 0" + }, + "rust": { + "name": "CaptureSourceType::None", + "signature": "None = 0" + } + } + }, + { + "key": "CaptureSourceType::Display", + "kind": "variant", + "doc": { + "summary": "One display." + }, + "signatures": { + "cpp": { + "name": "Display", + "signature": "Display = 1" + }, + "csharp": { + "name": "CaptureSourceType.Display", + "signature": "Display = 1" + }, + "dart": { + "name": "CaptureSourceType.display", + "signature": "display(1)" + }, + "go": { + "name": "CaptureSourceTypeDisplay", + "signature": "const CaptureSourceTypeDisplay CaptureSourceType = 1" + }, + "js": { + "name": "CaptureSourceType.Display", + "signature": "Display: 1" + }, + "python": { + "name": "DISPLAY", + "signature": "DISPLAY = 1" + }, + "rust": { + "name": "CaptureSourceType::Display", + "signature": "Display = 1" + } + } + }, + { + "key": "CaptureSourceType::Window", + "kind": "variant", + "doc": { + "summary": "One application window." + }, + "signatures": { + "cpp": { + "name": "Window", + "signature": "Window = 2" + }, + "csharp": { + "name": "CaptureSourceType.Window", + "signature": "Window = 2" + }, + "dart": { + "name": "CaptureSourceType.window", + "signature": "window(2)" + }, + "go": { + "name": "CaptureSourceTypeWindow", + "signature": "const CaptureSourceTypeWindow CaptureSourceType = 2" + }, + "js": { + "name": "CaptureSourceType.Window", + "signature": "Window: 2" + }, + "python": { + "name": "WINDOW", + "signature": "WINDOW = 2" + }, + "rust": { + "name": "CaptureSourceType::Window", + "signature": "Window = 2" + } + } + }, + { + "key": "CaptureSourceType::Unknown", + "kind": "variant", + "doc": { + "summary": "A valid system-selected source whose kind is not disclosed by the platform." + }, + "signatures": { + "cpp": { + "name": "Unknown", + "signature": "Unknown = 3" + }, + "csharp": { + "name": "CaptureSourceType.Unknown", + "signature": "Unknown = 3" + }, + "dart": { + "name": "CaptureSourceType.unknown", + "signature": "unknown(3)" + }, + "go": { + "name": "CaptureSourceTypeUnknown", + "signature": "const CaptureSourceTypeUnknown CaptureSourceType = 3" + }, + "js": { + "name": "CaptureSourceType.Unknown", + "signature": "Unknown: 3" + }, + "python": { + "name": "UNKNOWN", + "signature": "UNKNOWN = 3" + }, + "rust": { + "name": "CaptureSourceType::Unknown", + "signature": "Unknown = 3" + } + } + } + ] + } + ] +} diff --git a/website/content/api/index.json b/website/content/api/index.json index 5931f855..b8063250 100644 --- a/website/content/api/index.json +++ b/website/content/api/index.json @@ -663,6 +663,150 @@ } ] }, + { + "id": "capture_source", + "header": "capture_source.h", + "items": [ + { + "key": "CaptureSource", + "kind": "class", + "summary": "A capture target and its optional system authorization session.", + "languages": [ + "cpp", + "csharp", + "dart", + "go", + "js", + "python", + "rust" + ] + }, + { + "key": "CaptureSourceType", + "kind": "enum", + "summary": "The kind of content bound to a capture source.", + "languages": [ + "cpp", + "csharp", + "dart", + "go", + "js", + "python", + "rust" + ] + } + ] + }, + { + "id": "screen_capturer", + "header": "screen_capturer.h", + "items": [ + { + "key": "ScreenCapturer", + "kind": "class", + "summary": "Asynchronously selects capture targets and obtains independent SDR images.", + "languages": [ + "cpp", + "csharp", + "dart", + "go", + "js", + "python", + "rust" + ] + }, + { + "key": "CaptureSourceSelectionOptions", + "kind": "struct", + "summary": "Allowed source types and the owner of the system selection UI.", + "languages": [ + "cpp", + "csharp", + "dart", + "go", + "js", + "python", + "rust" + ] + }, + { + "key": "ScreenCaptureOptions", + "kind": "struct", + "summary": "Shared screenshot and recording configuration, copied when submitted.", + "languages": [ + "cpp", + "csharp", + "dart", + "go", + "js", + "python", + "rust" + ] + } + ] + }, + { + "id": "screen_recorder", + "header": "screen_recorder.h", + "items": [ + { + "key": "ScreenRecorder", + "kind": "class", + "summary": "Records one source as silent SDR H.264/MP4 using a native capture backend.", + "languages": [ + "cpp", + "csharp", + "dart", + "go", + "js", + "python", + "rust" + ] + }, + { + "key": "ScreenRecordingOptions", + "kind": "struct", + "summary": "Configuration for silent SDR H.264 video in an MP4 file.", + "languages": [ + "cpp", + "csharp", + "dart", + "go", + "js", + "python", + "rust" + ] + }, + { + "key": "ScreenRecordingState", + "kind": "enum", + "summary": "State of one recording attempt; terminal recorders cannot restart.", + "languages": [ + "cpp", + "csharp", + "dart", + "go", + "js", + "python", + "rust" + ] + }, + { + "key": "ScreenRecordingEvent", + "kind": "event", + "summary": "Base of recording notifications, emitted by the individual recorder.", + "languages": [ + "cpp", + "csharp", + "dart", + "go", + "js", + "python", + "rust" + ] + } + ] + }, { "id": "window_shape", "header": "window_shape.h", diff --git a/website/content/api/screen_capturer.json b/website/content/api/screen_capturer.json new file mode 100644 index 00000000..a62e2a03 --- /dev/null +++ b/website/content/api/screen_capturer.json @@ -0,0 +1,568 @@ +{ + "$comment": "AUTO-GENERATED. DO NOT EDIT. Generated by tools/codegen from the core headers; run ./codegen.", + "id": "screen_capturer", + "header": "screen_capturer.h", + "items": [ + { + "key": "ScreenCapturer", + "kind": "class", + "doc": { + "summary": "Asynchronously selects capture targets and obtains independent SDR images.", + "details": "Calls use the host UI thread and require a running event loop. Completions run on a later UI-loop turn exactly once. Without a dispatcher, failure is delivered synchronously on the caller thread. An empty callback does nothing. Runtime shutdown can revoke pending deliveries. Queries never prompt for permission.\n\n```cpp\nauto source = CaptureSource::FromDisplay(DisplayManager::GetInstance().GetPrimary());\nScreenCapturer::Capture(source, {}, [](bool ok, std::shared_ptr image) {\n if (ok) image->SaveToFile(\"/tmp/screenshot.png\");\n});\n```", + "notes": [ + { + "kind": "note", + "text": "Platform availability:\n- macOS: ⚠️ ScreenCaptureKit on macOS 14+; selection uses the system sharing picker\n- Windows: ⚠️ Windows 10 2004+; picker needs an owner window and both source types enabled; system-selected sources have type Unknown\n- Linux: ❌ Not implemented - Queries return false and operations fail\n- Android: ❌ Not implemented - Queries return false and operations fail\n- iOS: ❌ Not implemented - Queries return false and operations fail\n- OpenHarmony: ❌ Not implemented - Queries return false and operations fail" + } + ] + }, + "signatures": { + "cpp": { + "name": "ScreenCapturer", + "signature": "class ScreenCapturer" + }, + "csharp": { + "name": "ScreenCapturer", + "signature": "public static partial class ScreenCapturer" + }, + "dart": { + "name": "ScreenCapturer", + "signature": "class ScreenCapturer" + }, + "go": { + "name": "ScreenCapturer", + "signature": "type ScreenCapturer struct" + }, + "js": { + "name": "ScreenCapturer", + "signature": "export class ScreenCapturer" + }, + "python": { + "name": "ScreenCapturer", + "signature": "class ScreenCapturer:" + }, + "rust": { + "name": "ScreenCapturer", + "signature": "pub struct ScreenCapturer" + } + }, + "members": [ + { + "key": "ScreenCapturer::IsSupported", + "kind": "static", + "doc": { + "summary": "Checks for a usable capture backend, not permission or target validity." + }, + "signatures": { + "cpp": { + "name": "IsSupported", + "signature": "static bool IsSupported()" + }, + "csharp": { + "name": "IsSupported", + "signature": "public static bool IsSupported()" + }, + "dart": { + "name": "isSupported", + "signature": "static bool isSupported()" + }, + "go": { + "name": "ScreenCapturerIsSupported", + "signature": "func ScreenCapturerIsSupported() bool" + }, + "js": { + "name": "isSupported", + "signature": "static isSupported(): boolean" + }, + "python": { + "name": "is_supported", + "signature": "@staticmethod\ndef is_supported() -> bool:" + }, + "rust": { + "name": "is_supported", + "signature": "pub fn is_supported() -> bool" + } + } + }, + { + "key": "ScreenCapturer::IsSourceSelectionSupported", + "kind": "static", + "doc": { + "summary": "Checks for an available interactive source selection backend." + }, + "signatures": { + "cpp": { + "name": "IsSourceSelectionSupported", + "signature": "static bool IsSourceSelectionSupported()" + }, + "csharp": { + "name": "IsSourceSelectionSupported", + "signature": "public static bool IsSourceSelectionSupported()" + }, + "dart": { + "name": "isSourceSelectionSupported", + "signature": "static bool isSourceSelectionSupported()" + }, + "go": { + "name": "ScreenCapturerIsSourceSelectionSupported", + "signature": "func ScreenCapturerIsSourceSelectionSupported() bool" + }, + "js": { + "name": "isSourceSelectionSupported", + "signature": "static isSourceSelectionSupported(): boolean" + }, + "python": { + "name": "is_source_selection_supported", + "signature": "@staticmethod\ndef is_source_selection_supported() -> bool:" + }, + "rust": { + "name": "is_source_selection_supported", + "signature": "pub fn is_source_selection_supported() -> bool" + } + } + }, + { + "key": "ScreenCapturer::IsDirectCaptureSupported", + "kind": "static", + "doc": { + "summary": "Checks whether a target kind can be specified without the picker.", + "params": [ + { + "name": "type", + "text": "Kind to test; None always returns false." + } + ] + }, + "signatures": { + "cpp": { + "name": "IsDirectCaptureSupported", + "signature": "static bool IsDirectCaptureSupported(CaptureSourceType type)" + }, + "csharp": { + "name": "IsDirectCaptureSupported", + "signature": "public static bool IsDirectCaptureSupported(CaptureSourceType type)" + }, + "dart": { + "name": "isDirectCaptureSupported", + "signature": "static bool isDirectCaptureSupported(CaptureSourceType type)" + }, + "go": { + "name": "ScreenCapturerIsDirectCaptureSupported", + "signature": "func ScreenCapturerIsDirectCaptureSupported(kind CaptureSourceType) bool" + }, + "js": { + "name": "isDirectCaptureSupported", + "signature": "static isDirectCaptureSupported(type: CaptureSourceType): boolean" + }, + "python": { + "name": "is_direct_capture_supported", + "signature": "@staticmethod\ndef is_direct_capture_supported(type: _capture_source.CaptureSourceType) -> bool:" + }, + "rust": { + "name": "is_direct_capture_supported", + "signature": "pub fn is_direct_capture_supported(r#type: CaptureSourceType) -> bool" + } + } + }, + { + "key": "ScreenCapturer::SelectSource", + "kind": "static", + "doc": { + "summary": "Selects exactly one source in system UI; only one picker may be pending.", + "params": [ + { + "name": "options", + "text": "Allowed types and picker owner." + }, + { + "name": "callback", + "text": "(true, source) on selection, (true, nullptr) on user cancellation, (false, nullptr) on invalid input, denial, unsupported operation or failure." + } + ] + }, + "signatures": { + "cpp": { + "name": "SelectSource", + "signature": "static void SelectSource(CaptureSourceSelectionOptions options, std::function callback)" + }, + "csharp": { + "name": "SelectSource", + "signature": "public static void SelectSource(CaptureSourceSelectionOptions options, Action callback)" + }, + "dart": { + "name": "selectSource", + "signature": "static void selectSource(CaptureSourceSelectionOptions options, void Function(bool, CaptureSource?) callback)" + }, + "go": { + "name": "ScreenCapturerSelectSource", + "signature": "func ScreenCapturerSelectSource(options CaptureSourceSelectionOptions, callback func(bool, *CaptureSource))" + }, + "js": { + "name": "selectSource", + "signature": "static selectSource(options: CaptureSourceSelectionOptions, callback: (arg0: boolean, arg1: CaptureSource | null) => void): void" + }, + "python": { + "name": "select_source", + "signature": "@staticmethod\ndef select_source(\n options: CaptureSourceSelectionOptions,\n callback: Callable[[bool, _capture_source.CaptureSource | None], None],\n) -> None:" + }, + "rust": { + "name": "select_source", + "signature": "pub fn select_source(options: &CaptureSourceSelectionOptions, callback: impl Fn(bool, Option) + 'static)" + } + } + }, + { + "key": "ScreenCapturer::Capture", + "kind": "static", + "doc": { + "summary": "Captures a new frame, optionally cropping it, without saving a file.", + "params": [ + { + "name": "source", + "text": "Target retained until completion; null or busy sources fail." + }, + { + "name": "options", + "text": "Physical-pixel crop and cursor policy." + }, + { + "name": "callback", + "text": "(true, independent image) on success, (false, nullptr) on failure. A returned image remains usable after the source and native frame are released." + } + ] + }, + "signatures": { + "cpp": { + "name": "Capture", + "signature": "static void Capture(std::shared_ptr source, ScreenCaptureOptions options, std::function callback)" + }, + "csharp": { + "name": "Capture", + "signature": "public static void Capture(CaptureSource? source, ScreenCaptureOptions options, Action callback)" + }, + "dart": { + "name": "capture", + "signature": "static void capture(CaptureSource? source, ScreenCaptureOptions options, void Function(bool, Image?) callback)" + }, + "go": { + "name": "ScreenCapturerCapture", + "signature": "func ScreenCapturerCapture(source *CaptureSource, options ScreenCaptureOptions, callback func(bool, *Image))" + }, + "js": { + "name": "capture", + "signature": "static capture(source: CaptureSource | null, options: ScreenCaptureOptions, callback: (arg0: boolean, arg1: Image | null) => void): void" + }, + "python": { + "name": "capture", + "signature": "@staticmethod\ndef capture(\n source: _capture_source.CaptureSource | None,\n options: ScreenCaptureOptions,\n callback: Callable[[bool, _image.Image | None], None],\n) -> None:" + }, + "rust": { + "name": "capture", + "signature": "pub fn capture(source: Option<&CaptureSource>, options: &ScreenCaptureOptions, callback: impl Fn(bool, Option) + 'static)" + } + } + } + ] + }, + { + "key": "CaptureSourceSelectionOptions", + "kind": "struct", + "doc": { + "summary": "Allowed source types and the owner of the system selection UI." + }, + "signatures": { + "cpp": { + "name": "CaptureSourceSelectionOptions", + "signature": "struct CaptureSourceSelectionOptions" + }, + "csharp": { + "name": "CaptureSourceSelectionOptions", + "signature": "public struct CaptureSourceSelectionOptions" + }, + "dart": { + "name": "CaptureSourceSelectionOptions", + "signature": "class CaptureSourceSelectionOptions" + }, + "go": { + "name": "CaptureSourceSelectionOptions", + "signature": "type CaptureSourceSelectionOptions struct" + }, + "js": { + "name": "CaptureSourceSelectionOptions", + "signature": "export interface CaptureSourceSelectionOptions" + }, + "python": { + "name": "CaptureSourceSelectionOptions", + "signature": "@dataclass\nclass CaptureSourceSelectionOptions:" + }, + "rust": { + "name": "CaptureSourceSelectionOptions", + "signature": "pub struct CaptureSourceSelectionOptions" + } + }, + "members": [ + { + "key": "CaptureSourceSelectionOptions::allow_displays", + "kind": "field", + "doc": { + "summary": "Allow a display; at least one source type must be allowed." + }, + "signatures": { + "cpp": { + "name": "allow_displays", + "signature": "bool allow_displays" + }, + "csharp": { + "name": "AllowDisplays", + "signature": "public bool AllowDisplays;" + }, + "dart": { + "name": "allowDisplays", + "signature": "final bool allowDisplays" + }, + "go": { + "name": "AllowDisplays", + "signature": "AllowDisplays bool" + }, + "js": { + "name": "allowDisplays", + "signature": "allowDisplays: boolean" + }, + "python": { + "name": "allow_displays", + "signature": "allow_displays: bool = False" + }, + "rust": { + "name": "allow_displays", + "signature": "pub allow_displays: bool" + } + } + }, + { + "key": "CaptureSourceSelectionOptions::allow_windows", + "kind": "field", + "doc": { + "summary": "Allow a window, including other applications' windows." + }, + "signatures": { + "cpp": { + "name": "allow_windows", + "signature": "bool allow_windows" + }, + "csharp": { + "name": "AllowWindows", + "signature": "public bool AllowWindows;" + }, + "dart": { + "name": "allowWindows", + "signature": "final bool allowWindows" + }, + "go": { + "name": "AllowWindows", + "signature": "AllowWindows bool" + }, + "js": { + "name": "allowWindows", + "signature": "allowWindows: boolean" + }, + "python": { + "name": "allow_windows", + "signature": "allow_windows: bool = False" + }, + "rust": { + "name": "allow_windows", + "signature": "pub allow_windows: bool" + } + } + }, + { + "key": "CaptureSourceSelectionOptions::parent_window", + "kind": "field", + "doc": { + "summary": "Owner of the picker, not the captured window; nullptr uses the current window." + }, + "signatures": { + "cpp": { + "name": "parent_window", + "signature": "std::shared_ptr parent_window" + }, + "csharp": { + "name": "ParentWindow", + "signature": "public Window? ParentWindow;" + }, + "dart": { + "name": "parentWindow", + "signature": "final Window? parentWindow" + }, + "go": { + "name": "ParentWindow", + "signature": "ParentWindow *Window" + }, + "js": { + "name": "parentWindow", + "signature": "parentWindow?: Window | null" + }, + "python": { + "name": "parent_window", + "signature": "parent_window: _window.Window | None = None" + }, + "rust": { + "name": "parent_window", + "signature": "pub parent_window: Option" + } + } + } + ] + }, + { + "key": "ScreenCaptureOptions", + "kind": "struct", + "doc": { + "summary": "Shared screenshot and recording configuration, copied when submitted." + }, + "signatures": { + "cpp": { + "name": "ScreenCaptureOptions", + "signature": "struct ScreenCaptureOptions" + }, + "csharp": { + "name": "ScreenCaptureOptions", + "signature": "public struct ScreenCaptureOptions" + }, + "dart": { + "name": "ScreenCaptureOptions", + "signature": "class ScreenCaptureOptions" + }, + "go": { + "name": "ScreenCaptureOptions", + "signature": "type ScreenCaptureOptions struct" + }, + "js": { + "name": "ScreenCaptureOptions", + "signature": "export interface ScreenCaptureOptions" + }, + "python": { + "name": "ScreenCaptureOptions", + "signature": "@dataclass\nclass ScreenCaptureOptions:" + }, + "rust": { + "name": "ScreenCaptureOptions", + "signature": "pub struct ScreenCaptureOptions" + } + }, + "members": [ + { + "key": "ScreenCaptureOptions::include_cursor", + "kind": "field", + "doc": { + "summary": "Include the pointer in the captured pixels; unsupported modes fail." + }, + "signatures": { + "cpp": { + "name": "include_cursor", + "signature": "bool include_cursor" + }, + "csharp": { + "name": "IncludeCursor", + "signature": "public bool IncludeCursor;" + }, + "dart": { + "name": "includeCursor", + "signature": "final bool includeCursor" + }, + "go": { + "name": "IncludeCursor", + "signature": "IncludeCursor bool" + }, + "js": { + "name": "includeCursor", + "signature": "includeCursor: boolean" + }, + "python": { + "name": "include_cursor", + "signature": "include_cursor: bool = False" + }, + "rust": { + "name": "include_cursor", + "signature": "pub include_cursor: bool" + } + } + }, + { + "key": "ScreenCaptureOptions::crop_enabled", + "kind": "field", + "doc": { + "summary": "Apply crop; when false the rectangle is ignored." + }, + "signatures": { + "cpp": { + "name": "crop_enabled", + "signature": "bool crop_enabled" + }, + "csharp": { + "name": "CropEnabled", + "signature": "public bool CropEnabled;" + }, + "dart": { + "name": "cropEnabled", + "signature": "final bool cropEnabled" + }, + "go": { + "name": "CropEnabled", + "signature": "CropEnabled bool" + }, + "js": { + "name": "cropEnabled", + "signature": "cropEnabled: boolean" + }, + "python": { + "name": "crop_enabled", + "signature": "crop_enabled: bool = False" + }, + "rust": { + "name": "crop_enabled", + "signature": "pub crop_enabled: bool" + } + } + }, + { + "key": "ScreenCaptureOptions::crop", + "kind": "field", + "doc": { + "summary": "Source-local physical pixels, top-left origin, not global logical coordinates. All components must be finite integers, x/y >= 0, width/height > 0, in bounds." + }, + "signatures": { + "cpp": { + "name": "crop", + "signature": "Rectangle crop" + }, + "csharp": { + "name": "Crop", + "signature": "public Rectangle Crop;" + }, + "dart": { + "name": "crop", + "signature": "final Rectangle crop" + }, + "go": { + "name": "Crop", + "signature": "Crop Rectangle" + }, + "js": { + "name": "crop", + "signature": "crop: Rectangle" + }, + "python": { + "name": "crop", + "signature": "crop: _geometry.Rectangle = field(default_factory=lambda: _geometry.Rectangle())" + }, + "rust": { + "name": "crop", + "signature": "pub crop: Rectangle" + } + } + } + ] + } + ] +} diff --git a/website/content/api/screen_recorder.json b/website/content/api/screen_recorder.json new file mode 100644 index 00000000..3d9d50f8 --- /dev/null +++ b/website/content/api/screen_recorder.json @@ -0,0 +1,1183 @@ +{ + "$comment": "AUTO-GENERATED. DO NOT EDIT. Generated by tools/codegen from the core headers; run ./codegen.", + "id": "screen_recorder", + "header": "screen_recorder.h", + "items": [ + { + "key": "ScreenRecorder", + "kind": "class", + "doc": { + "summary": "Records one source as silent SDR H.264/MP4 using a native capture backend.", + "details": "Calls use the host UI thread. Start/Stop/Cancel report acceptance, not completion. Keep the recorder alive until a terminal event. Destruction discards unfinished output and suppresses further events; it never implicitly saves. No listener is required for recording to progress. Output dimensions are fixed by the first frame; resized content is fitted with black bars. Requested crops must remain in bounds.\n\n```cpp\nauto recorder = std::make_shared();\nauto source = CaptureSource::FromDisplay(DisplayManager::GetInstance().GetPrimary());\nbool accepted = recorder->Start(source, \"/tmp/recording.mp4\", {});\n// Retain recorder; after StartedEvent call Stop and await a terminal event.\n```", + "notes": [ + { + "kind": "note", + "text": "Platform availability:\n- macOS: ⚠️ macOS 14+ ScreenCaptureKit and AVAssetWriter; permission required\n- Windows: ⚠️ Windows 10 2004+ Graphics Capture and Media Foundation H.264 encoder\n- Linux: ❌ Not implemented - Start returns false\n- Android: ❌ Not implemented - Start returns false\n- iOS: ❌ Not implemented - Start returns false\n- OpenHarmony: ❌ Not implemented - Start returns false" + } + ] + }, + "signatures": { + "cpp": { + "name": "ScreenRecorder", + "signature": "class ScreenRecorder : public EventEmitter" + }, + "csharp": { + "name": "ScreenRecorder", + "signature": "public sealed partial class ScreenRecorder : IDisposable" + }, + "dart": { + "name": "ScreenRecorder", + "signature": "class ScreenRecorder implements ffi.Finalizable" + }, + "go": { + "name": "ScreenRecorder", + "signature": "type ScreenRecorder struct" + }, + "js": { + "name": "ScreenRecorder", + "signature": "export class ScreenRecorder extends NativeObject" + }, + "python": { + "name": "ScreenRecorder", + "signature": "class ScreenRecorder(_rt.NativeObject):" + }, + "rust": { + "name": "ScreenRecorder", + "signature": "pub struct ScreenRecorder" + } + }, + "members": [ + { + "key": "ScreenRecorder::new", + "kind": "constructor", + "signatures": { + "cpp": { + "name": "new", + "signature": "ScreenRecorder()" + }, + "csharp": { + "name": "Create", + "signature": "public static ScreenRecorder? Create()" + }, + "dart": { + "name": "create", + "signature": "static ScreenRecorder? create()" + }, + "go": { + "name": "NewScreenRecorder", + "signature": "func NewScreenRecorder() (*ScreenRecorder, error)" + }, + "js": { + "name": "create", + "signature": "static create(): ScreenRecorder | null" + }, + "python": { + "name": "__init__", + "signature": "def __init__(self) -> None:" + }, + "rust": { + "name": "new", + "signature": "pub fn new() -> Option" + } + } + }, + { + "key": "ScreenRecorder::listener", + "kind": "listener", + "signatures": { + "cpp": { + "name": "listener", + "signature": "size_t AddListener(std::function listener)" + }, + "csharp": { + "name": "AddListener", + "signature": "public ulong AddListener(Action callback)\npublic bool RemoveListener(ulong listenerId)" + }, + "dart": { + "name": "addListener", + "signature": "ListenerId addListener(FutureOr Function(ScreenRecordingEvent) callback)\nbool removeListener(ListenerId listenerId)" + }, + "go": { + "name": "AddListener", + "signature": "func (sr *ScreenRecorder) AddListener(callback func(ScreenRecordingEvent)) (ListenerID, error)\nfunc (sr *ScreenRecorder) RemoveListener(id ListenerID) error" + }, + "js": { + "name": "addListener", + "signature": "addListener(listener: (event: ScreenRecordingEvent) => void | Promise): number\nremoveListener(listenerId: number): boolean" + }, + "python": { + "name": "add_listener", + "signature": "def add_listener(self, callback: Callable[[ScreenRecordingEvent], None]) -> int:\ndef remove_listener(self, listener_id: int) -> bool:" + }, + "rust": { + "name": "add_listener", + "signature": "pub fn add_listener(&self, callback: impl Fn(&ScreenRecordingEvent) + 'static) -> ListenerId\npub fn remove_listener(&self, listener_id: ListenerId) -> bool" + } + } + }, + { + "key": "ScreenRecorder::IsSupported", + "kind": "static", + "doc": { + "summary": "Checks for capture and H.264/MP4 encoding support, without prompting." + }, + "signatures": { + "cpp": { + "name": "IsSupported", + "signature": "static bool IsSupported()" + }, + "csharp": { + "name": "IsSupported", + "signature": "public static bool IsSupported()" + }, + "dart": { + "name": "isSupported", + "signature": "static bool isSupported()" + }, + "go": { + "name": "ScreenRecorderIsSupported", + "signature": "func ScreenRecorderIsSupported() bool" + }, + "js": { + "name": "isSupported", + "signature": "static isSupported(): boolean" + }, + "python": { + "name": "is_supported", + "signature": "@staticmethod\ndef is_supported() -> bool:" + }, + "rust": { + "name": "is_supported", + "signature": "pub fn is_supported() -> bool" + } + } + }, + { + "key": "ScreenRecorder::Start", + "kind": "method", + "doc": { + "summary": "Starts one recording asynchronously from Idle.", + "params": [ + { + "name": "source", + "text": "Target retained until termination." + }, + { + "name": "file_path", + "text": "Absolute UTF-8 local .mp4 path; existing entries are never replaced." + }, + { + "name": "options", + "text": "Sampling, bitrate, crop and cursor configuration." + } + ], + "returns": "false on synchronous rejection; state remains Idle and no event is emitted. true means Starting; later failures are reported by ScreenRecordingFailedEvent." + }, + "signatures": { + "cpp": { + "name": "Start", + "signature": "bool Start(std::shared_ptr source, std::string file_path, ScreenRecordingOptions options)" + }, + "csharp": { + "name": "Start", + "signature": "public bool Start(CaptureSource? source, string filePath, ScreenRecordingOptions options)" + }, + "dart": { + "name": "start", + "signature": "bool start(CaptureSource? source, String filePath, ScreenRecordingOptions options)" + }, + "go": { + "name": "Start", + "signature": "func (sr *ScreenRecorder) Start(source *CaptureSource, filePath string, options ScreenRecordingOptions) error" + }, + "js": { + "name": "start", + "signature": "start(source: CaptureSource | null, filePath: string, options: ScreenRecordingOptions): boolean" + }, + "python": { + "name": "start", + "signature": "def start(\n self,\n source: _capture_source.CaptureSource | None,\n file_path: str,\n options: ScreenRecordingOptions,\n) -> bool:" + }, + "rust": { + "name": "start", + "signature": "pub fn start(&self, source: Option<&CaptureSource>, file_path: &str, options: &ScreenRecordingOptions) -> bool" + } + } + }, + { + "key": "ScreenRecorder::Stop", + "kind": "method", + "doc": { + "summary": "Stops a Recording session and asynchronously publishes the finished file.", + "returns": "false outside Recording. Await CompletedEvent or FailedEvent after true." + }, + "signatures": { + "cpp": { + "name": "Stop", + "signature": "bool Stop()" + }, + "csharp": { + "name": "Stop", + "signature": "public bool Stop()" + }, + "dart": { + "name": "stop", + "signature": "bool stop()" + }, + "go": { + "name": "Stop", + "signature": "func (sr *ScreenRecorder) Stop() error" + }, + "js": { + "name": "stop", + "signature": "stop(): boolean" + }, + "python": { + "name": "stop", + "signature": "def stop(self) -> bool:" + }, + "rust": { + "name": "stop", + "signature": "pub fn stop(&self) -> bool" + } + } + }, + { + "key": "ScreenRecorder::Cancel", + "kind": "method", + "doc": { + "summary": "Discards a Starting or Recording session asynchronously.", + "returns": "false in other states, including Stopping. Await CancelledEvent or FailedEvent." + }, + "signatures": { + "cpp": { + "name": "Cancel", + "signature": "bool Cancel()" + }, + "csharp": { + "name": "Cancel", + "signature": "public bool Cancel()" + }, + "dart": { + "name": "cancel", + "signature": "bool cancel()" + }, + "go": { + "name": "Cancel", + "signature": "func (sr *ScreenRecorder) Cancel() error" + }, + "js": { + "name": "cancel", + "signature": "cancel(): boolean" + }, + "python": { + "name": "cancel", + "signature": "def cancel(self) -> bool:" + }, + "rust": { + "name": "cancel", + "signature": "pub fn cancel(&self) -> bool" + } + } + }, + { + "key": "ScreenRecorder::GetState", + "kind": "method", + "doc": { + "summary": "Gets the current recording state." + }, + "signatures": { + "cpp": { + "name": "GetState", + "signature": "ScreenRecordingState GetState() const" + }, + "csharp": { + "name": "State", + "signature": "public ScreenRecordingState State" + }, + "dart": { + "name": "state", + "signature": "ScreenRecordingState get state" + }, + "go": { + "name": "State", + "signature": "func (sr *ScreenRecorder) State() ScreenRecordingState" + }, + "js": { + "name": "state", + "signature": "get state(): ScreenRecordingState" + }, + "python": { + "name": "state", + "signature": "@property\ndef state(self) -> ScreenRecordingState:" + }, + "rust": { + "name": "state", + "signature": "pub fn state(&self) -> ScreenRecordingState" + } + } + }, + { + "key": "ScreenRecorder::IsActive", + "kind": "method", + "doc": { + "summary": "Checks for Starting, Recording or Stopping." + }, + "signatures": { + "cpp": { + "name": "IsActive", + "signature": "bool IsActive() const" + }, + "csharp": { + "name": "IsActive", + "signature": "public bool IsActive" + }, + "dart": { + "name": "isActive", + "signature": "bool get isActive" + }, + "go": { + "name": "IsActive", + "signature": "func (sr *ScreenRecorder) IsActive() bool" + }, + "js": { + "name": "isActive", + "signature": "get isActive(): boolean" + }, + "python": { + "name": "is_active", + "signature": "@property\ndef is_active(self) -> bool:" + }, + "rust": { + "name": "is_active", + "signature": "pub fn is_active(&self) -> bool" + } + } + }, + { + "key": "ScreenRecorder::GetDuration", + "kind": "method", + "doc": { + "summary": "Gets media duration in seconds; zero before the first frame. On failure/cancellation this is diagnostic, not a promise of a saved file." + }, + "signatures": { + "cpp": { + "name": "GetDuration", + "signature": "double GetDuration() const" + }, + "csharp": { + "name": "Duration", + "signature": "public double Duration" + }, + "dart": { + "name": "duration", + "signature": "double get duration" + }, + "go": { + "name": "Duration", + "signature": "func (sr *ScreenRecorder) Duration() float64" + }, + "js": { + "name": "duration", + "signature": "get duration(): number" + }, + "python": { + "name": "duration", + "signature": "@property\ndef duration(self) -> float:" + }, + "rust": { + "name": "duration", + "signature": "pub fn duration(&self) -> f64" + } + } + }, + { + "key": "ScreenRecorder::GetFilePath", + "kind": "method", + "doc": { + "summary": "Gets the requested absolute path, empty before acceptance. Only Completed guarantees that a finalized file was published there." + }, + "signatures": { + "cpp": { + "name": "GetFilePath", + "signature": "std::string GetFilePath() const" + }, + "csharp": { + "name": "FilePath", + "signature": "public string? FilePath" + }, + "dart": { + "name": "filePath", + "signature": "String? get filePath" + }, + "go": { + "name": "FilePath", + "signature": "func (sr *ScreenRecorder) FilePath() string" + }, + "js": { + "name": "filePath", + "signature": "get filePath(): string" + }, + "python": { + "name": "file_path", + "signature": "@property\ndef file_path(self) -> str:" + }, + "rust": { + "name": "file_path", + "signature": "pub fn file_path(&self) -> Option" + } + } + } + ] + }, + { + "key": "ScreenRecordingOptions", + "kind": "struct", + "doc": { + "summary": "Configuration for silent SDR H.264 video in an MP4 file." + }, + "signatures": { + "cpp": { + "name": "ScreenRecordingOptions", + "signature": "struct ScreenRecordingOptions" + }, + "csharp": { + "name": "ScreenRecordingOptions", + "signature": "public struct ScreenRecordingOptions" + }, + "dart": { + "name": "ScreenRecordingOptions", + "signature": "class ScreenRecordingOptions" + }, + "go": { + "name": "ScreenRecordingOptions", + "signature": "type ScreenRecordingOptions struct" + }, + "js": { + "name": "ScreenRecordingOptions", + "signature": "export interface ScreenRecordingOptions" + }, + "python": { + "name": "ScreenRecordingOptions", + "signature": "@dataclass\nclass ScreenRecordingOptions:" + }, + "rust": { + "name": "ScreenRecordingOptions", + "signature": "pub struct ScreenRecordingOptions" + } + }, + "members": [ + { + "key": "ScreenRecordingOptions::capture", + "kind": "field", + "doc": { + "summary": "Cursor and source-local physical-pixel crop configuration." + }, + "signatures": { + "cpp": { + "name": "capture", + "signature": "ScreenCaptureOptions capture" + }, + "csharp": { + "name": "Capture", + "signature": "public ScreenCaptureOptions Capture;" + }, + "dart": { + "name": "capture", + "signature": "final ScreenCaptureOptions capture" + }, + "go": { + "name": "Capture", + "signature": "Capture ScreenCaptureOptions" + }, + "js": { + "name": "capture", + "signature": "capture: ScreenCaptureOptions" + }, + "python": { + "name": "capture", + "signature": "capture: _screen_capturer.ScreenCaptureOptions = (\n field(default_factory=lambda: _screen_capturer.ScreenCaptureOptions())\n)" + }, + "rust": { + "name": "capture", + "signature": "pub capture: ScreenCaptureOptions" + } + } + }, + { + "key": "ScreenRecordingOptions::frame_rate", + "kind": "field", + "doc": { + "summary": "Finite maximum sampling rate in frames per second, in [0.1, 240]." + }, + "signatures": { + "cpp": { + "name": "frame_rate", + "signature": "double frame_rate" + }, + "csharp": { + "name": "FrameRate", + "signature": "public double FrameRate;" + }, + "dart": { + "name": "frameRate", + "signature": "final double frameRate" + }, + "go": { + "name": "FrameRate", + "signature": "FrameRate float64" + }, + "js": { + "name": "frameRate", + "signature": "frameRate: number" + }, + "python": { + "name": "frame_rate", + "signature": "frame_rate: float = 0.0" + }, + "rust": { + "name": "frame_rate", + "signature": "pub frame_rate: f64" + } + } + }, + { + "key": "ScreenRecordingOptions::video_bitrate", + "kind": "field", + "doc": { + "summary": "Target bits per second, at most UINT32_MAX; zero selects an encoder default." + }, + "signatures": { + "cpp": { + "name": "video_bitrate", + "signature": "unsigned long video_bitrate" + }, + "csharp": { + "name": "VideoBitrate", + "signature": "public ulong VideoBitrate;" + }, + "dart": { + "name": "videoBitrate", + "signature": "final int videoBitrate" + }, + "go": { + "name": "VideoBitrate", + "signature": "VideoBitrate uint64" + }, + "js": { + "name": "videoBitrate", + "signature": "videoBitrate: number" + }, + "python": { + "name": "video_bitrate", + "signature": "video_bitrate: int = 0" + }, + "rust": { + "name": "video_bitrate", + "signature": "pub video_bitrate: std::os::raw::c_ulong" + } + } + } + ] + }, + { + "key": "ScreenRecordingState", + "kind": "enum", + "doc": { + "summary": "State of one recording attempt; terminal recorders cannot restart." + }, + "signatures": { + "cpp": { + "name": "ScreenRecordingState", + "signature": "enum class ScreenRecordingState" + }, + "csharp": { + "name": "ScreenRecordingState", + "signature": "public enum ScreenRecordingState" + }, + "dart": { + "name": "ScreenRecordingState", + "signature": "enum ScreenRecordingState" + }, + "go": { + "name": "ScreenRecordingState", + "signature": "type ScreenRecordingState int32" + }, + "js": { + "name": "ScreenRecordingState", + "signature": "export const ScreenRecordingState =\nexport type ScreenRecordingState = (typeof ScreenRecordingState)[keyof typeof ScreenRecordingState]" + }, + "python": { + "name": "ScreenRecordingState", + "signature": "class ScreenRecordingState(enum.IntEnum):" + }, + "rust": { + "name": "ScreenRecordingState", + "signature": "pub enum ScreenRecordingState" + } + }, + "members": [ + { + "key": "ScreenRecordingState::Idle", + "kind": "variant", + "doc": { + "summary": "No request has been accepted." + }, + "signatures": { + "cpp": { + "name": "Idle", + "signature": "Idle = 0" + }, + "csharp": { + "name": "ScreenRecordingState.Idle", + "signature": "Idle = 0" + }, + "dart": { + "name": "ScreenRecordingState.idle", + "signature": "idle(0)" + }, + "go": { + "name": "ScreenRecordingStateIdle", + "signature": "const ScreenRecordingStateIdle ScreenRecordingState = 0" + }, + "js": { + "name": "ScreenRecordingState.Idle", + "signature": "Idle: 0" + }, + "python": { + "name": "IDLE", + "signature": "IDLE = 0" + }, + "rust": { + "name": "ScreenRecordingState::Idle", + "signature": "Idle = 0" + } + } + }, + { + "key": "ScreenRecordingState::Starting", + "kind": "variant", + "doc": { + "summary": "Starting capture and the encoder." + }, + "signatures": { + "cpp": { + "name": "Starting", + "signature": "Starting = 1" + }, + "csharp": { + "name": "ScreenRecordingState.Starting", + "signature": "Starting = 1" + }, + "dart": { + "name": "ScreenRecordingState.starting", + "signature": "starting(1)" + }, + "go": { + "name": "ScreenRecordingStateStarting", + "signature": "const ScreenRecordingStateStarting ScreenRecordingState = 1" + }, + "js": { + "name": "ScreenRecordingState.Starting", + "signature": "Starting: 1" + }, + "python": { + "name": "STARTING", + "signature": "STARTING = 1" + }, + "rust": { + "name": "ScreenRecordingState::Starting", + "signature": "Starting = 1" + } + } + }, + { + "key": "ScreenRecordingState::Recording", + "kind": "variant", + "doc": { + "summary": "At least one frame has been accepted by the encoder." + }, + "signatures": { + "cpp": { + "name": "Recording", + "signature": "Recording = 2" + }, + "csharp": { + "name": "ScreenRecordingState.Recording", + "signature": "Recording = 2" + }, + "dart": { + "name": "ScreenRecordingState.recording", + "signature": "recording(2)" + }, + "go": { + "name": "ScreenRecordingStateRecording", + "signature": "const ScreenRecordingStateRecording ScreenRecordingState = 2" + }, + "js": { + "name": "ScreenRecordingState.Recording", + "signature": "Recording: 2" + }, + "python": { + "name": "RECORDING", + "signature": "RECORDING = 2" + }, + "rust": { + "name": "ScreenRecordingState::Recording", + "signature": "Recording = 2" + } + } + }, + { + "key": "ScreenRecordingState::Stopping", + "kind": "variant", + "doc": { + "summary": "Finalizing output or discarding a cancelled recording." + }, + "signatures": { + "cpp": { + "name": "Stopping", + "signature": "Stopping = 3" + }, + "csharp": { + "name": "ScreenRecordingState.Stopping", + "signature": "Stopping = 3" + }, + "dart": { + "name": "ScreenRecordingState.stopping", + "signature": "stopping(3)" + }, + "go": { + "name": "ScreenRecordingStateStopping", + "signature": "const ScreenRecordingStateStopping ScreenRecordingState = 3" + }, + "js": { + "name": "ScreenRecordingState.Stopping", + "signature": "Stopping: 3" + }, + "python": { + "name": "STOPPING", + "signature": "STOPPING = 3" + }, + "rust": { + "name": "ScreenRecordingState::Stopping", + "signature": "Stopping = 3" + } + } + }, + { + "key": "ScreenRecordingState::Completed", + "kind": "variant", + "doc": { + "summary": "The finished file has been published." + }, + "signatures": { + "cpp": { + "name": "Completed", + "signature": "Completed = 4" + }, + "csharp": { + "name": "ScreenRecordingState.Completed", + "signature": "Completed = 4" + }, + "dart": { + "name": "ScreenRecordingState.completed", + "signature": "completed(4)" + }, + "go": { + "name": "ScreenRecordingStateCompleted", + "signature": "const ScreenRecordingStateCompleted ScreenRecordingState = 4" + }, + "js": { + "name": "ScreenRecordingState.Completed", + "signature": "Completed: 4" + }, + "python": { + "name": "COMPLETED", + "signature": "COMPLETED = 4" + }, + "rust": { + "name": "ScreenRecordingState::Completed", + "signature": "Completed = 4" + } + } + }, + { + "key": "ScreenRecordingState::Cancelled", + "kind": "variant", + "doc": { + "summary": "Cancellation and cleanup completed." + }, + "signatures": { + "cpp": { + "name": "Cancelled", + "signature": "Cancelled = 5" + }, + "csharp": { + "name": "ScreenRecordingState.Cancelled", + "signature": "Cancelled = 5" + }, + "dart": { + "name": "ScreenRecordingState.cancelled", + "signature": "cancelled(5)" + }, + "go": { + "name": "ScreenRecordingStateCancelled", + "signature": "const ScreenRecordingStateCancelled ScreenRecordingState = 5" + }, + "js": { + "name": "ScreenRecordingState.Cancelled", + "signature": "Cancelled: 5" + }, + "python": { + "name": "CANCELLED", + "signature": "CANCELLED = 5" + }, + "rust": { + "name": "ScreenRecordingState::Cancelled", + "signature": "Cancelled = 5" + } + } + }, + { + "key": "ScreenRecordingState::Failed", + "kind": "variant", + "doc": { + "summary": "Capture, encoding, publication or cleanup failed." + }, + "signatures": { + "cpp": { + "name": "Failed", + "signature": "Failed = 6" + }, + "csharp": { + "name": "ScreenRecordingState.Failed", + "signature": "Failed = 6" + }, + "dart": { + "name": "ScreenRecordingState.failed", + "signature": "failed(6)" + }, + "go": { + "name": "ScreenRecordingStateFailed", + "signature": "const ScreenRecordingStateFailed ScreenRecordingState = 6" + }, + "js": { + "name": "ScreenRecordingState.Failed", + "signature": "Failed: 6" + }, + "python": { + "name": "FAILED", + "signature": "FAILED = 6" + }, + "rust": { + "name": "ScreenRecordingState::Failed", + "signature": "Failed = 6" + } + } + } + ] + }, + { + "key": "ScreenRecordingEvent", + "kind": "event", + "doc": { + "summary": "Base of recording notifications, emitted by the individual recorder." + }, + "signatures": { + "cpp": { + "name": "ScreenRecordingEvent", + "signature": "class ScreenRecordingEvent : public Event" + }, + "csharp": { + "name": "ScreenRecordingEvent", + "signature": "public abstract record ScreenRecordingEvent" + }, + "dart": { + "name": "ScreenRecordingEvent", + "signature": "sealed class ScreenRecordingEvent" + }, + "go": { + "name": "ScreenRecordingEvent", + "signature": "type ScreenRecordingEvent struct" + }, + "js": { + "name": "ScreenRecordingEvent", + "signature": "export type ScreenRecordingEvent =" + }, + "python": { + "name": "ScreenRecordingEvent", + "signature": "@dataclass(frozen=True)\nclass ScreenRecordingEvent:" + }, + "rust": { + "name": "ScreenRecordingEvent", + "signature": "pub enum ScreenRecordingEvent" + } + }, + "members": [ + { + "key": "ScreenRecordingStartedEvent", + "kind": "variant", + "doc": { + "summary": "The first video frame has been accepted." + }, + "signatures": { + "cpp": { + "name": "ScreenRecordingStartedEvent", + "signature": "class ScreenRecordingStartedEvent : public ScreenRecordingEvent" + }, + "csharp": { + "name": "ScreenRecordingEvent.Started", + "signature": "public sealed record Started : ScreenRecordingEvent;" + }, + "dart": { + "name": "ScreenRecordingStartedEvent", + "signature": "final class ScreenRecordingStartedEvent extends ScreenRecordingEvent" + }, + "go": { + "name": "ScreenRecordingEventStarted", + "signature": "const ScreenRecordingEventStarted ScreenRecordingEventType = 0" + }, + "js": { + "name": "Extract", + "signature": "| { type: \"started\" }" + }, + "python": { + "name": "ScreenRecordingStartedEvent", + "signature": "@dataclass(frozen=True)\nclass ScreenRecordingStartedEvent(ScreenRecordingEvent):" + }, + "rust": { + "name": "ScreenRecordingEvent::Started", + "signature": "Started" + } + } + }, + { + "key": "ScreenRecordingCompletedEvent", + "kind": "variant", + "doc": { + "summary": "Encoding, file closing and non-overwriting publication succeeded." + }, + "signatures": { + "cpp": { + "name": "ScreenRecordingCompletedEvent", + "signature": "class ScreenRecordingCompletedEvent : public ScreenRecordingEvent" + }, + "csharp": { + "name": "ScreenRecordingEvent.Completed", + "signature": "public sealed record Completed(string? FilePath, double Duration) : ScreenRecordingEvent;" + }, + "dart": { + "name": "ScreenRecordingCompletedEvent", + "signature": "final class ScreenRecordingCompletedEvent extends ScreenRecordingEvent" + }, + "go": { + "name": "ScreenRecordingEventCompleted", + "signature": "const ScreenRecordingEventCompleted ScreenRecordingEventType = 1" + }, + "js": { + "name": "Extract", + "signature": "| { type: \"completed\"; filePath: string | null; duration: number }" + }, + "python": { + "name": "ScreenRecordingCompletedEvent", + "signature": "@dataclass(frozen=True)\nclass ScreenRecordingCompletedEvent(ScreenRecordingEvent):" + }, + "rust": { + "name": "ScreenRecordingEvent::Completed", + "signature": "Completed { file_path: Option, duration: f64 }" + } + } + }, + { + "key": "ScreenRecordingCompletedEvent::FilePath", + "kind": "field", + "parent": "ScreenRecordingCompletedEvent", + "doc": { + "summary": "Gets the finalized absolute file path." + }, + "signatures": { + "cpp": { + "name": "FilePath", + "signature": "std::string GetFilePath() const" + }, + "csharp": { + "name": "FilePath", + "signature": "string? FilePath" + }, + "dart": { + "name": "filePath", + "signature": "final String? filePath" + }, + "go": { + "name": "FilePath", + "signature": "FilePath string" + }, + "js": { + "name": "filePath", + "signature": "filePath: string | null" + }, + "python": { + "name": "file_path", + "signature": "file_path: str" + }, + "rust": { + "name": "file_path", + "signature": "file_path: Option" + } + } + }, + { + "key": "ScreenRecordingCompletedEvent::Duration", + "kind": "field", + "parent": "ScreenRecordingCompletedEvent", + "doc": { + "summary": "Gets the media duration in seconds, excluding authorization time." + }, + "signatures": { + "cpp": { + "name": "Duration", + "signature": "double GetDuration() const" + }, + "csharp": { + "name": "Duration", + "signature": "double Duration" + }, + "dart": { + "name": "duration", + "signature": "final double duration" + }, + "go": { + "name": "Duration", + "signature": "Duration float64" + }, + "js": { + "name": "duration", + "signature": "duration: number" + }, + "python": { + "name": "duration", + "signature": "duration: float" + }, + "rust": { + "name": "duration", + "signature": "duration: f64" + } + } + }, + { + "key": "ScreenRecordingCancelledEvent", + "kind": "variant", + "doc": { + "summary": "The recording was discarded and temporary output cleaned up." + }, + "signatures": { + "cpp": { + "name": "ScreenRecordingCancelledEvent", + "signature": "class ScreenRecordingCancelledEvent : public ScreenRecordingEvent" + }, + "csharp": { + "name": "ScreenRecordingEvent.Cancelled", + "signature": "public sealed record Cancelled : ScreenRecordingEvent;" + }, + "dart": { + "name": "ScreenRecordingCancelledEvent", + "signature": "final class ScreenRecordingCancelledEvent extends ScreenRecordingEvent" + }, + "go": { + "name": "ScreenRecordingEventCancelled", + "signature": "const ScreenRecordingEventCancelled ScreenRecordingEventType = 2" + }, + "js": { + "name": "Extract", + "signature": "| { type: \"cancelled\" }" + }, + "python": { + "name": "ScreenRecordingCancelledEvent", + "signature": "@dataclass(frozen=True)\nclass ScreenRecordingCancelledEvent(ScreenRecordingEvent):" + }, + "rust": { + "name": "ScreenRecordingEvent::Cancelled", + "signature": "Cancelled" + } + } + }, + { + "key": "ScreenRecordingFailedEvent", + "kind": "variant", + "doc": { + "summary": "A recording failed; diagnostics are not stable machine-readable codes." + }, + "signatures": { + "cpp": { + "name": "ScreenRecordingFailedEvent", + "signature": "class ScreenRecordingFailedEvent : public ScreenRecordingEvent" + }, + "csharp": { + "name": "ScreenRecordingEvent.Failed", + "signature": "public sealed record Failed(string? Message) : ScreenRecordingEvent;" + }, + "dart": { + "name": "ScreenRecordingFailedEvent", + "signature": "final class ScreenRecordingFailedEvent extends ScreenRecordingEvent" + }, + "go": { + "name": "ScreenRecordingEventFailed", + "signature": "const ScreenRecordingEventFailed ScreenRecordingEventType = 3" + }, + "js": { + "name": "Extract", + "signature": "| { type: \"failed\"; message: string | null }" + }, + "python": { + "name": "ScreenRecordingFailedEvent", + "signature": "@dataclass(frozen=True)\nclass ScreenRecordingFailedEvent(ScreenRecordingEvent):" + }, + "rust": { + "name": "ScreenRecordingEvent::Failed", + "signature": "Failed { message: Option }" + } + } + }, + { + "key": "ScreenRecordingFailedEvent::Message", + "kind": "field", + "parent": "ScreenRecordingFailedEvent", + "doc": { + "summary": "Gets a human-readable diagnostic, including cleanup failure when relevant." + }, + "signatures": { + "cpp": { + "name": "Message", + "signature": "std::string GetMessage() const" + }, + "csharp": { + "name": "Message", + "signature": "string? Message" + }, + "dart": { + "name": "message", + "signature": "final String? message" + }, + "go": { + "name": "Message", + "signature": "Message string" + }, + "js": { + "name": "message", + "signature": "message: string | null" + }, + "python": { + "name": "message", + "signature": "message: str" + }, + "rust": { + "name": "message", + "signature": "message: Option" + } + } + } + ] + } + ] +}