- 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).
136 lines
5.4 KiB
Plaintext
136 lines
5.4 KiB
Plaintext
// What both ways of taking the screen share (dxgi_source.jai, wgc_source.jai): a D3D11 device of
|
|
// their own on the adapter the screen is attached to, the region as last seen, copied out of each
|
|
// new image on the GPU, and the staging textures it is read back through.
|
|
|
|
Capture_Gpu :: struct {
|
|
adapter: *IDXGIAdapter1;
|
|
device: *ID3D11Device;
|
|
device_context: *ID3D11DeviceContext;
|
|
box: D3D11_BOX; // the region, in the screen's pixels
|
|
latest: *ID3D11Texture2D; // the region as last seen
|
|
staging: [RECORD_STAGING] *ID3D11Texture2D;
|
|
copy_ticks: s64; // spent taking the region out of images (QueryPerformanceCounter ticks)
|
|
}
|
|
|
|
RECORD_STAGING :: 3; // staging textures in turn: the one read is two frames behind the one copied
|
|
|
|
// What a source's next() found.
|
|
Capture_Next :: enum u8 { NONE; IMAGE; POINTER; IMAGE_AND_POINTER; LOST; FAILED; }
|
|
|
|
// The device on the screen's adapter, and the textures for a region (in the screen's pixels).
|
|
gpu_open :: (g: *Capture_Gpu, screen: Record_Screen, region: RECT) -> ok: bool, error: string {
|
|
factory: *IDXGIFactory1;
|
|
hr := CreateDXGIFactory1(*uid(IDXGIFactory1_UUID), xx *factory);
|
|
if FAILED(hr) return false, hresult_text("CreateDXGIFactory1", hr);
|
|
defer IUnknown_Release(factory);
|
|
hr = IDXGIFactory1_EnumAdapters1(factory, screen.adapter_index, *g.adapter);
|
|
if FAILED(hr) return false, hresult_text("the screen's adapter is gone", hr);
|
|
|
|
levels := D3D_FEATURE_LEVEL.[._11_1, ._11_0, ._10_1, ._10_0];
|
|
level: D3D_FEATURE_LEVEL;
|
|
hr = D3D11CreateDevice(g.adapter, .UNKNOWN, null, .BGRA_SUPPORT, levels.data, levels.count, D3D11_SDK_VERSION, *g.device, *level, *g.device_context);
|
|
if FAILED(hr) return false, hresult_text("D3D11CreateDevice on the screen's adapter", hr);
|
|
|
|
g.box = .{left = xx region.left, top = xx region.top, right = xx region.right, bottom = xx region.bottom, front = 0, back = 1};
|
|
desc: D3D11_TEXTURE2D_DESC;
|
|
desc.Width = cast(u32) (region.right - region.left);
|
|
desc.Height = cast(u32) (region.bottom - region.top);
|
|
desc.MipLevels = 1;
|
|
desc.ArraySize = 1;
|
|
desc.Format = .B8G8R8A8_UNORM;
|
|
desc.SampleDesc.Count = 1;
|
|
desc.Usage = .DEFAULT;
|
|
hr = ID3D11Device_CreateTexture2D(g.device, *desc, null, *g.latest);
|
|
if FAILED(hr) return false, hresult_text("a texture for the region", hr);
|
|
desc.Usage = .STAGING;
|
|
desc.CPUAccessFlags = .READ;
|
|
for * g.staging {
|
|
hr = ID3D11Device_CreateTexture2D(g.device, *desc, null, it);
|
|
if FAILED(hr) return false, hresult_text("a staging texture", hr);
|
|
}
|
|
return true, "";
|
|
}
|
|
|
|
// The region of a whole screen's image into g.latest.
|
|
gpu_copy_region :: (g: *Capture_Gpu, image: *ID3D11Texture2D) {
|
|
start := ticks_now();
|
|
ID3D11DeviceContext_CopySubresourceRegion(g.device_context, g.latest, 0, 0, 0, 0, image, 0, *g.box);
|
|
g.copy_ticks += ticks_now() - start;
|
|
}
|
|
|
|
gpu_close :: (g: *Capture_Gpu) {
|
|
for * g.staging com_release(it);
|
|
com_release(*g.latest);
|
|
com_release(*g.device_context);
|
|
com_release(*g.device);
|
|
com_release(*g.adapter);
|
|
}
|
|
|
|
// The calling thread made aware of each monitor's DPI (Windows 10 1703 on), so the desktop it is told
|
|
// of is in the screens' own pixels.
|
|
gpu_thread_dpi_aware :: () {
|
|
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 :: cast(HANDLE) -4;
|
|
SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
|
}
|
|
|
|
#scope_module
|
|
|
|
user32_dll :: #library,system "user32";
|
|
SetThreadDpiAwarenessContext :: (value: HANDLE) -> HANDLE #foreign user32_dll;
|
|
|
|
// A GUID from its text, "A95664D2-9614-4F35-A746-DE8DB63617E6": for #run.
|
|
guid :: (text: string) -> GUID {
|
|
hex :: (s: string) -> u64 {
|
|
value: u64;
|
|
for 0..s.count - 1 {
|
|
c := s[it];
|
|
digit: u64 = ifx c >= #char "a" then cast(u64) (c - #char "a" + 10) else ifx c >= #char "A" then cast(u64) (c - #char "A" + 10) else cast(u64) (c - #char "0");
|
|
value = value * 16 + digit;
|
|
}
|
|
return value;
|
|
}
|
|
g: GUID;
|
|
g.Data1 = cast(u32) hex(slice(text, 0, 8));
|
|
g.Data2 = cast(u16) hex(slice(text, 9, 4));
|
|
g.Data3 = cast(u16) hex(slice(text, 14, 4));
|
|
for 0..1 g.Data4[it] = cast(u8) hex(slice(text, 19 + it * 2, 2));
|
|
for 0..5 g.Data4[it + 2] = cast(u8) hex(slice(text, 24 + it * 2, 2));
|
|
return g;
|
|
}
|
|
|
|
com_release :: (p: *$T) {
|
|
if p.* { IUnknown_Release(cast(*IUnknown) p.*); p.* = null; }
|
|
}
|
|
|
|
ticks_now :: () -> s64 {
|
|
t: LARGE_INTEGER;
|
|
QueryPerformanceCounter(*t);
|
|
return t.QuadPart;
|
|
}
|
|
|
|
hresult_is :: (hr: HRESULT, code: u32) -> bool {
|
|
return cast,no_check(u32) hr == code;
|
|
}
|
|
|
|
// "what failed: 0x887A0004 (DXGI_ERROR_UNSUPPORTED)", temp.
|
|
hresult_text :: (what: string, hr: HRESULT) -> string {
|
|
code := cast,no_check(u32) hr;
|
|
name := "";
|
|
if code == {
|
|
case DXGI_ERROR_UNSUPPORTED; name = " (DXGI_ERROR_UNSUPPORTED)";
|
|
case DXGI_ERROR_NOT_CURRENTLY_AVAILABLE; name = " (DXGI_ERROR_NOT_CURRENTLY_AVAILABLE)";
|
|
case DXGI_ERROR_ACCESS_LOST; name = " (DXGI_ERROR_ACCESS_LOST)";
|
|
case 0x80070005; name = " (E_ACCESSDENIED)";
|
|
case 0x80004002; name = " (E_NOINTERFACE)";
|
|
case 0x80070057; name = " (E_INVALIDARG)";
|
|
case 0x80040154; name = " (REGDB_E_CLASSNOTREG)";
|
|
case 0x80004001; name = " (E_NOTIMPL)";
|
|
}
|
|
return tprint("%: 0x%%", what, formatInt(code, base = 16, minimum_digits = 8), name);
|
|
}
|
|
|
|
wide_text :: (wide: *s16) -> string {
|
|
text, ok := wide_to_utf8(cast(*u16) wide,, temp);
|
|
return ifx ok then text else "";
|
|
}
|