mcap/modules/Screen_Record/wgc_source.jai
Musa Mahmood 72f2754226 [0.1.8] Video capture phase 5: sound, from the computer and the microphone, in sync
- modules/Screen_Record/audio_source.jai: WASAPI, in loopback from the default output device
  (what the computer plays) and from the default input device (the microphone), asked for as
  48 kHz stereo float, which Windows converts any device to; a device that will not convert is
  taken as it is when it gives 48 kHz float. Packets carry the time they were heard. A device
  lost (unplugged, default changed) is opened again from the new default once a second.
- An audio thread mixes them onto the video's own timeline: the same time 0, less the same
  pauses (record_pause now keeps one account of time paused for both threads). A packet within
  30 ms of where its source left off follows on exactly; a source that sends nothing is silence,
  passed 200 ms late. Each 1024 samples every source has covered become an AAC frame.
- encoder.jai: an AAC stream (FFmpeg's aac, 160 kb/s) beside the video, written from the audio
  thread; both threads' packets meet in the file under a lock. The remux keeps it.
- [video] system_audio (true) and microphone (false). capture.json: Audio, AudioSources. A toast
  when a source asked for could not be opened.
- record-probe: -audio system|mic|both, the sound read back (length, level), and -sync-test,
  which flashes a white square and beeps together every 2 s, then finds both in the file.
- Measured: 20 s, beeps 0-21 ms from their flashes, no drift; both sources mixed, 5-18 ms;
  a 6 s pause cuts the same 6 s from sound and video; 3 min, 85 of 90 beeps within 3-24 ms and
  -14 ms from first to last, with 5 outliers (53-147 ms) where two beeps went missing. Tests:
  an MP4 with sound keeps its AAC stream through the remux into the library (30 checks).
2026-10-10 08:08:47 +01:00

389 lines
18 KiB
Plaintext

// Windows.Graphics.Capture: the screen as Windows' own capture API hands it over, for when Desktop
// Duplication cannot run (dxgi_source.jai). It works across GPUs, hands over frames the right way up
// on a rotated screen, and draws the cursor in itself.
//
// It is WinRT, bound here by hand: the few interfaces it needs, as vtables after IInspectable's six
// methods. Everything is made and used on the capture thread, which is in the multithreaded apartment
// (RoInitialize), with a free-threaded frame pool, so no dispatcher queue or message loop is needed.
//
// A frame's arrival sets an event, through a small handler object (Frame_Handler); if Windows will not
// take the handler, the pool is polled every millisecond instead. Windows 11 lets the yellow border it
// draws around a screen being captured be turned off (IsBorderRequired); Windows 10 always draws it.
Wgc_Source :: struct {
#as using gpu: Capture_Gpu;
screen_size: Size_Int32; // the frames' size, as the pool was made for
item: *Capture_Item;
winrt_device: *IInspectable; // IDirect3DDevice, over gpu.device
pool: *Frame_Pool;
session: *Capture_Session;
handler: Frame_Handler;
handler_token: s64;
polling: bool; // Windows would not take the handler
border: string; // what came of turning the border off, for the log
}
// Starts capturing the screen. region is in the screen's pixels and lies on it.
wgc_open :: (s: *Wgc_Source, screen: Record_Screen, region: RECT, cursor: bool, testing: Record_Testing = 0) -> ok: bool, error: string {
ok, error := gpu_open(s, screen, region);
if !ok return false, error;
session_statics: *Session_Statics;
hr := activation_factory("Windows.Graphics.Capture.GraphicsCaptureSession", IID_SESSION_STATICS, xx *session_statics);
if FAILED(hr) return false, hresult_text("Windows.Graphics.Capture is not on this system", hr);
supported: u8;
session_statics.vtable.IsSupported(session_statics, *supported);
com_release(*session_statics);
if !supported return false, "Windows.Graphics.Capture is not supported here";
interop: *Capture_Item_Interop;
hr = activation_factory("Windows.Graphics.Capture.GraphicsCaptureItem", IID_ITEM_INTEROP, xx *interop);
if FAILED(hr) return false, hresult_text("IGraphicsCaptureItemInterop", hr);
hr = interop.vtable.CreateForMonitor(interop, screen.monitor, *IID_ITEM, xx *s.item);
com_release(*interop);
if FAILED(hr) return false, hresult_text("a capture item for the screen", hr);
s.item.vtable.get_Size(s.item, *s.screen_size);
desktop_width := screen.desktop.right - screen.desktop.left;
desktop_height := screen.desktop.bottom - screen.desktop.top;
if s.screen_size.width != desktop_width || s.screen_size.height != desktop_height {
return false, tprint("the screen is % x %, but this process sees it as % x %: it must be aware of each monitor's DPI",
s.screen_size.width, s.screen_size.height, desktop_width, desktop_height);
}
dxgi_device: *IDXGIDevice;
hr = IUnknown_QueryInterface(s.device, *uid(IDXGIDevice_UUID), xx *dxgi_device);
if FAILED(hr) return false, hresult_text("the device's IDXGIDevice", hr);
inspectable: *IInspectable;
hr = CreateDirect3D11DeviceFromDXGIDevice(dxgi_device, *inspectable);
com_release(*dxgi_device);
if FAILED(hr) return false, hresult_text("CreateDirect3D11DeviceFromDXGIDevice", hr);
hr = inspectable.vtable.QueryInterface(inspectable, *IID_DIRECT3D_DEVICE, xx *s.winrt_device);
com_release(*inspectable);
if FAILED(hr) return false, hresult_text("the device as IDirect3DDevice", hr);
pool_statics: *Frame_Pool_Statics2;
hr = activation_factory("Windows.Graphics.Capture.Direct3D11CaptureFramePool", IID_POOL_STATICS2, xx *pool_statics);
if FAILED(hr) return false, hresult_text("Direct3D11CaptureFramePool's statics", hr);
hr = pool_statics.vtable.CreateFreeThreaded(pool_statics, s.winrt_device, PIXEL_FORMAT_B8G8R8A8, POOL_BUFFERS, size_packed(s.screen_size), *s.pool);
com_release(*pool_statics);
if FAILED(hr) return false, hresult_text("a free-threaded frame pool", hr);
hr = s.pool.vtable.CreateCaptureSession(s.pool, s.item, *s.session);
if FAILED(hr) return false, hresult_text("a capture session", hr);
// The cursor (Windows 10 2004 on) and the border (Windows 11): optional, by what the system has.
session2: *Capture_Session2;
if SUCCEEDED(s.session.vtable.QueryInterface(s.session, *IID_SESSION2, xx *session2)) {
session2.vtable.put_IsCursorCaptureEnabled(session2, xx cursor);
com_release(*session2);
}
session3: *Capture_Session3;
if testing & .KEEP_BORDER {
s.border = "border left on";
} else if SUCCEEDED(s.session.vtable.QueryInterface(s.session, *IID_SESSION3, xx *session3)) {
hr = session3.vtable.put_IsBorderRequired(session3, 0);
s.border = ifx SUCCEEDED(hr) then "border off" else hresult_text("the border could not be turned off", hr);
com_release(*session3);
} else {
s.border = "border always drawn (no IGraphicsCaptureSession3: Windows 10)";
}
s.handler.vtable = *frame_handler_vtable;
s.handler.event = CreateEventW(null, 0, 0, null);
hr = ifx testing & .WGC_POLL then cast,no_check(HRESULT) 0x80004002 else s.pool.vtable.add_FrameArrived(s.pool, *s.handler, *s.handler_token);
if FAILED(hr) {
s.polling = true;
timeBeginPeriod(1); // so polling every millisecond is every millisecond
log("[record] FrameArrived would not take the handler (%); polling the frame pool.", hresult_text("add_FrameArrived", hr));
}
hr = s.session.vtable.StartCapture(s.session);
if FAILED(hr) return false, hresult_text("StartCapture", hr);
return true, "";
}
// Waits up to timeout_ms for a new frame; the newest's region goes into s.latest.
wgc_next :: (s: *Wgc_Source, timeout_ms: u32) -> Capture_Next, error: string {
frame := wgc_take_newest(s);
if !frame {
if s.polling {
start := ticks_now();
frequency: LARGE_INTEGER;
QueryPerformanceFrequency(*frequency);
while !frame && (ticks_now() - start) * 1000 / frequency.QuadPart < timeout_ms {
sleep_milliseconds(1);
frame = wgc_take_newest(s);
}
} else {
WaitForSingleObject(s.handler.event, timeout_ms);
frame = wgc_take_newest(s);
}
}
if !frame return .NONE, "";
defer close_and_release(frame);
// The screen changed size (a new resolution): the pool is made again; the region must still fit.
size: Size_Int32;
frame.vtable.get_ContentSize(frame, *size);
if size.width != s.screen_size.width || size.height != s.screen_size.height {
s.screen_size = size;
hr := s.pool.vtable.Recreate(s.pool, s.winrt_device, PIXEL_FORMAT_B8G8R8A8, POOL_BUFFERS, size_packed(size));
if FAILED(hr) return .FAILED, hresult_text("the frame pool could not be made again", hr);
if cast(s32) s.box.right > size.width || cast(s32) s.box.bottom > size.height
return .FAILED, tprint("the screen is now % x %, and the region is not on it", size.width, size.height);
return .NONE, "";
}
surface: *IInspectable;
hr := frame.vtable.get_Surface(frame, *surface);
if FAILED(hr) return .FAILED, hresult_text("the frame's surface", hr);
defer com_release(*surface);
access: *Dxgi_Interface_Access;
hr = surface.vtable.QueryInterface(surface, *IID_DXGI_INTERFACE_ACCESS, xx *access);
if FAILED(hr) return .FAILED, hresult_text("the surface's IDirect3DDxgiInterfaceAccess", hr);
defer com_release(*access);
texture: *ID3D11Texture2D;
hr = access.vtable.GetInterface(access, *uid(ID3D11Texture2D_UUID), xx *texture);
if FAILED(hr) return .FAILED, hresult_text("the surface's texture", hr);
gpu_copy_region(s, texture);
IUnknown_Release(texture);
return .IMAGE, "";
}
wgc_close :: (s: *Wgc_Source) {
if s.pool && s.handler_token s.pool.vtable.remove_FrameArrived(s.pool, s.handler_token);
if s.session close_and_release(s.session);
if s.pool close_and_release(s.pool);
s.session, s.pool = null, null;
com_release(*s.item);
com_release(*s.winrt_device);
if s.handler.event CloseHandle(s.handler.event);
if s.polling timeEndPeriod(1);
gpu_close(s);
s.* = .{};
}
// The thread's apartment: WinRT on this thread from here on.
wgc_thread_start :: () -> ok: bool {
if !winrt_load() return false;
return SUCCEEDED(winrt.RoInitialize(RO_INIT_MULTITHREADED));
}
wgc_thread_end :: () {
if winrt.RoUninitialize winrt.RoUninitialize();
}
#scope_file
POOL_BUFFERS :: 2;
PIXEL_FORMAT_B8G8R8A8 :: 87; // DirectXPixelFormat.B8G8R8A8UIntNormalized
RO_INIT_MULTITHREADED :: 1;
// The pool's newest frame, the older ones let go; null when there is none.
wgc_take_newest :: (s: *Wgc_Source) -> *Capture_Frame {
newest: *Capture_Frame;
while true {
frame: *Capture_Frame;
if FAILED(s.pool.vtable.TryGetNextFrame(s.pool, *frame)) || !frame break;
if newest close_and_release(newest);
newest = frame;
}
return newest;
}
// IClosable.Close, which hands a frame back to the pool at once, then Release.
close_and_release :: (object: *$T) {
closable: *Closable;
if SUCCEEDED(object.vtable.QueryInterface(object, *IID_CLOSABLE, xx *closable)) {
closable.vtable.Close(closable);
closable.vtable.Release(closable);
}
object.vtable.Release(object);
}
activation_factory :: (class_name: string, iid: GUID, factory: **void) -> HRESULT {
wide := utf8_to_wide(class_name,, temp);
length: u32;
while wide[length] length += 1;
name: HSTRING;
if !winrt.loaded return cast,no_check(HRESULT) 0x80004001; // E_NOTIMPL: no WinRT on this system
hr := winrt.WindowsCreateString(wide, length, *name);
if FAILED(hr) return hr;
defer winrt.WindowsDeleteString(name);
iid_copy := iid;
return winrt.RoGetActivationFactory(name, *iid_copy, factory);
}
size_packed :: (size: Size_Int32) -> u64 { // SizeInt32 by value: 8 bytes, in one register
width := cast(u64) cast,no_check(u32) size.width;
height := cast(u64) cast,no_check(u32) size.height;
return width | (height << 32);
}
// ---------------------------------------------------------------- the FrameArrived handler
// TypedEventHandler<Direct3D11CaptureFramePool, Object>: a COM object of our own whose Invoke sets an
// event. It lives in the Wgc_Source and is never freed by its count; it is unregistered first.
Frame_Handler :: struct {
vtable: *Frame_Handler_VTable;
event: HANDLE;
}
Frame_Handler_VTable :: struct {
QueryInterface: (this: *Frame_Handler, iid: *GUID, out: **void) -> HRESULT #c_call;
AddRef: (this: *Frame_Handler) -> u32 #c_call;
Release: (this: *Frame_Handler) -> u32 #c_call;
Invoke: (this: *Frame_Handler, sender: *void, args: *void) -> HRESULT #c_call;
}
frame_handler_vtable := Frame_Handler_VTable.{handler_query, handler_add_ref, handler_release, handler_invoke};
handler_query :: (this: *Frame_Handler, iid: *GUID, out: **void) -> HRESULT #c_call {
if guid_equal(iid, *IID_UNKNOWN) || guid_equal(iid, *IID_AGILE_OBJECT) || guid_equal(iid, *IID_FRAME_ARRIVED_HANDLER) {
out.* = this;
return 0;
}
out.* = null;
return cast,no_check(HRESULT) 0x80004002; // E_NOINTERFACE
}
handler_add_ref :: (this: *Frame_Handler) -> u32 #c_call { return 1; }
handler_release :: (this: *Frame_Handler) -> u32 #c_call { return 1; }
handler_invoke :: (this: *Frame_Handler, sender: *void, args: *void) -> HRESULT #c_call {
SetEvent(this.event);
return 0;
}
guid_equal :: (a: *GUID, b: *GUID) -> bool #c_call {
if a.Data1 != b.Data1 || a.Data2 != b.Data2 || a.Data3 != b.Data3 return false;
for 0..7 if a.Data4[it] != b.Data4[it] return false;
return true;
}
// ---------------------------------------------------------------- bindings
d3d11_dll :: #library,system "d3d11";
kernel32_dll :: #library,system "kernel32";
HSTRING :: *void;
// WinRT's own calls, from combase.dll at run time: its import library is runtimeobject.lib, which
// names no DLL of its own for #library to find.
winrt: struct {
loaded: bool;
RoInitialize: (init_type: s32) -> HRESULT #c_call;
RoUninitialize: () #c_call;
RoGetActivationFactory: (class_id: HSTRING, iid: *GUID, factory: **void) -> HRESULT #c_call;
WindowsCreateString: (source: *u16, length: u32, out: *HSTRING) -> HRESULT #c_call;
WindowsDeleteString: (text: HSTRING) -> HRESULT #c_call;
}
winrt_load :: () -> bool {
if winrt.loaded return true;
library := LoadLibraryW(utf8_to_wide("combase.dll",, temp));
if !library return false;
winrt.RoInitialize = xx GetProcAddress(library, "RoInitialize");
winrt.RoUninitialize = xx GetProcAddress(library, "RoUninitialize");
winrt.RoGetActivationFactory = xx GetProcAddress(library, "RoGetActivationFactory");
winrt.WindowsCreateString = xx GetProcAddress(library, "WindowsCreateString");
winrt.WindowsDeleteString = xx GetProcAddress(library, "WindowsDeleteString");
winrt.loaded = winrt.RoInitialize && winrt.RoUninitialize && winrt.RoGetActivationFactory && winrt.WindowsCreateString && winrt.WindowsDeleteString;
return winrt.loaded;
}
CreateDirect3D11DeviceFromDXGIDevice :: (device: *IDXGIDevice, out: **IInspectable) -> HRESULT #foreign d3d11_dll;
SetEvent :: (event: HANDLE) -> s32 #foreign kernel32_dll;
Size_Int32 :: struct { width, height: s32; }
IID_UNKNOWN := #run guid("00000000-0000-0000-C000-000000000046");
IID_AGILE_OBJECT := #run guid("94ea2b94-e9cc-49e0-c0ff-ee64ca8f5b90");
IID_FRAME_ARRIVED_HANDLER := #run guid("51a947f7-79cf-5a3e-a3a5-1289cfa6dfe8"); // TypedEventHandler<Direct3D11CaptureFramePool, Object>
IID_ITEM_INTEROP := #run guid("3628E81B-3CAC-4C60-B7F4-23CE0E0C3356");
IID_ITEM := #run guid("79C3F95B-31F7-4EC2-A464-632EF5D30760");
IID_SESSION_STATICS := #run guid("2224A540-5974-49AA-B232-0882536F4CB5");
IID_SESSION2 := #run guid("2C39AE40-7D2E-5044-804E-8B6799D4CF9E");
IID_SESSION3 := #run guid("F2CDD966-22AE-5EA1-9596-3A289344C3BE");
IID_POOL_STATICS2 := #run guid("589B103F-6BBC-5DF5-A991-02E28B3B66D5");
IID_DIRECT3D_DEVICE := #run guid("A37624AB-8D5F-4650-9D3E-9EAE3D9BC670");
IID_DXGI_INTERFACE_ACCESS := #run guid("A9B3D012-3DF2-4EE3-B8D1-8695F457D3C1");
IID_CLOSABLE := #run guid("30D5A829-7FA4-4026-83BB-D75BAE4EA99E");
// IUnknown, then IInspectable's three: every WinRT interface starts so.
Inspectable_Methods :: struct {
QueryInterface: (this: *void, iid: *GUID, out: **void) -> HRESULT #c_call;
AddRef: (this: *void) -> u32 #c_call;
Release: (this: *void) -> u32 #c_call;
GetIids: *void;
GetRuntimeClassName: *void;
GetTrustLevel: *void;
}
IInspectable :: struct { vtable: *Inspectable_Methods; }
Closable :: struct { vtable: *struct { using base: Inspectable_Methods; Close: (this: *Closable) -> HRESULT #c_call; }; }
Session_Statics :: struct { vtable: *struct { using base: Inspectable_Methods; IsSupported: (this: *Session_Statics, out: *u8) -> HRESULT #c_call; }; }
Capture_Item_Interop :: struct { vtable: *struct {
QueryInterface: (this: *void, iid: *GUID, out: **void) -> HRESULT #c_call;
AddRef: (this: *void) -> u32 #c_call;
Release: (this: *void) -> u32 #c_call;
CreateForWindow: (this: *Capture_Item_Interop, window: HWND, iid: *GUID, out: **void) -> HRESULT #c_call;
CreateForMonitor: (this: *Capture_Item_Interop, monitor: HMONITOR, iid: *GUID, out: **void) -> HRESULT #c_call;
}; }
Capture_Item :: struct { vtable: *struct {
using base: Inspectable_Methods;
get_DisplayName: *void;
get_Size: (this: *Capture_Item, out: *Size_Int32) -> HRESULT #c_call;
add_Closed: *void;
remove_Closed: *void;
}; }
Frame_Pool_Statics2 :: struct { vtable: *struct {
using base: Inspectable_Methods;
CreateFreeThreaded: (this: *Frame_Pool_Statics2, device: *IInspectable, format: s32, buffers: s32, size: u64, out: **Frame_Pool) -> HRESULT #c_call;
}; }
Frame_Pool :: struct { vtable: *struct {
using base: Inspectable_Methods;
Recreate: (this: *Frame_Pool, device: *IInspectable, format: s32, buffers: s32, size: u64) -> HRESULT #c_call;
TryGetNextFrame: (this: *Frame_Pool, out: **Capture_Frame) -> HRESULT #c_call;
add_FrameArrived: (this: *Frame_Pool, handler: *Frame_Handler, token: *s64) -> HRESULT #c_call;
remove_FrameArrived: (this: *Frame_Pool, token: s64) -> HRESULT #c_call;
CreateCaptureSession: (this: *Frame_Pool, item: *Capture_Item, out: **Capture_Session) -> HRESULT #c_call;
get_DispatcherQueue: *void;
}; }
Capture_Frame :: struct { vtable: *struct {
using base: Inspectable_Methods;
get_Surface: (this: *Capture_Frame, out: **IInspectable) -> HRESULT #c_call;
get_SystemRelativeTime: (this: *Capture_Frame, out: *s64) -> HRESULT #c_call;
get_ContentSize: (this: *Capture_Frame, out: *Size_Int32) -> HRESULT #c_call;
}; }
Capture_Session :: struct { vtable: *struct {
using base: Inspectable_Methods;
StartCapture: (this: *Capture_Session) -> HRESULT #c_call;
}; }
Capture_Session2 :: struct { vtable: *struct {
using base: Inspectable_Methods;
get_IsCursorCaptureEnabled: (this: *Capture_Session2, out: *u8) -> HRESULT #c_call;
put_IsCursorCaptureEnabled: (this: *Capture_Session2, value: u8) -> HRESULT #c_call;
}; }
Capture_Session3 :: struct { vtable: *struct {
using base: Inspectable_Methods;
get_IsBorderRequired: (this: *Capture_Session3, out: *u8) -> HRESULT #c_call;
put_IsBorderRequired: (this: *Capture_Session3, value: u8) -> HRESULT #c_call;
}; }
Dxgi_Interface_Access :: struct { vtable: *struct {
QueryInterface: (this: *void, iid: *GUID, out: **void) -> HRESULT #c_call;
AddRef: (this: *void) -> u32 #c_call;
Release: (this: *void) -> u32 #c_call;
GetInterface: (this: *Dxgi_Interface_Access, iid: *GUID, out: **void) -> HRESULT #c_call;
}; }