- The toolbar's Mic: on / off (red while on) turns the microphone on and off while recording; its state is saved as microphone in [video] for the next recording. A microphone that cannot be opened turns it off again, with a toast saying why. - modules/Screen_Record: record_set_microphone, record_microphone, record_microphone_failed. The audio thread opens the microphone only while it is on (so Windows shows it in use only then) and closes it when turned off. Record_Options.audio_track gives the MP4 a sound track whatever is on at the start, silent while nothing is, as one cannot be added once the header is written; the app always asks for it. The sources heard and the failures are kept as they happen, not only at the start. - record-probe -mic-toggle: the microphone on at 3 s and off at 7 s. With system sound and with nothing else on, the track runs the whole recording without a break.
617 lines
30 KiB
Plaintext
617 lines
30 KiB
Plaintext
// record-probe: phase 1 of video-capture-plan.html. Records a screen, or a region of one, with
|
|
// modules/Screen_Record (DXGI Desktop Duplication and FFmpeg) and says how it went: which screens
|
|
// there are and on which adapter, which duplication call and encoder worked, frames captured, written
|
|
// and dropped, time per stage, and the file's size. It also answers what the plan leaves to phase 1:
|
|
//
|
|
// -capture HOW auto (Desktop Duplication, else Windows.Graphics.Capture), dxgi or wgc.
|
|
// -border-test counts yellow pixels along the screen's edges, with GDI, before recording and a
|
|
// second in: does Windows draw its capture border (Windows.Graphics.Capture)?
|
|
// -keep-border leaves Windows.Graphics.Capture's border on: the border test's control.
|
|
// -simulate-unsupported Desktop Duplication fails as on a laptop with two GPUs: the fallback's path.
|
|
// -poll Windows.Graphics.Capture polled, as when it will not take its frame handler.
|
|
// -pause SECONDS pauses 2 s in, for that long: the video should be that much shorter, with no gap.
|
|
// -lose Desktop Duplication lost every 3 s, as a mode change or the secure desktop loses it.
|
|
// -cursor-test puts the mouse in the middle of the region while recording, and back after;
|
|
// with -snapshot, cursor.png is the frame around it, four times as large.
|
|
// -legacy the plain DuplicateOutput, not DuplicateOutput1: does it fail for a process aware
|
|
// of each monitor's DPI, as mcap is (this probe is, unless -dpi-unaware)?
|
|
// -cuda makes a CUDA context first, as onnxruntime does in mcap: does duplication still work?
|
|
// -exclude-test puts two squares on the region, one kept out of captures with
|
|
// WDA_EXCLUDEFROMCAPTURE: is it out of the recording, and the other in?
|
|
// -animate moves a square across the region the whole time, so the screen changes every
|
|
// frame: for measuring throughput.
|
|
// -remux IN OUT a recording's fragmented MP4 made a regular one, as the app does, and nothing else.
|
|
// -audio WHICH records sound too: system (what the computer plays), mic, or both.
|
|
// -mic-toggle with -audio system: the microphone turned on 3 s in, and off at 7 s, as the toolbar does.
|
|
// -sync-test every 2 s a white square flashes on the region and a 1 kHz beep plays; then the
|
|
// flashes and beeps are found in the file and their offsets given, and how they drift.
|
|
// -check FILE reads FILE back with FFmpeg and says what it holds, and nothing else: for a
|
|
// recording cut short by a crash.
|
|
// -snapshot DIR the last frame captured (captured.png) and a frame decoded back from the file
|
|
// (decoded.png), to compare by eye.
|
|
//
|
|
// Build: jai first.jai - record_probe
|
|
// Run: bin/record-probe [-list] [-screen N] [-region X Y W H] [-seconds S] [-fps F] [-encoder NAME]
|
|
// [-out FILE.mp4] [-no-cursor] [-legacy] [-cuda] [-exclude-test] [-snapshot DIR]
|
|
// [-animate] [-capture auto|dxgi|wgc] [-border-test] [-keep-border] [-cursor-test]
|
|
// [-simulate-unsupported] [-poll] [-pause SECONDS] [-lose] [-dpi-unaware]
|
|
// Exit code: 0 recorded, 1 failed, 3 Desktop Duplication cannot run here (the fallback's case).
|
|
|
|
main :: () {
|
|
args := get_command_line_arguments();
|
|
options: Record_Options;
|
|
options.path = tprint("%/record-probe.mp4", get_working_directory());
|
|
seconds: float64 = 10;
|
|
list_only, cuda, exclude_test, dpi_unaware, animate, border_test, cursor_test := false, false, false, false, false, false, false;
|
|
snapshot_folder: string;
|
|
pause_seconds: float64;
|
|
sync_test := false;
|
|
mic_toggle := false;
|
|
i := 1;
|
|
number :: (index: s64) -> s32 #expand { return cast(s32) string_to_int(`args[index]); }
|
|
while i < args.count {
|
|
a := args[i];
|
|
more := args.count - i - 1;
|
|
if a == "-list" { list_only = true; i += 1; continue; }
|
|
if a == "-screen" && more >= 1 { options.screen = number(i + 1); i += 2; continue; }
|
|
if a == "-region" && more >= 4 {
|
|
x, y, w, h := number(i + 1), number(i + 2), number(i + 3), number(i + 4);
|
|
options.region = .{x, y, x + w, y + h};
|
|
i += 5; continue;
|
|
}
|
|
if a == "-seconds" && more >= 1 { seconds = string_to_float64(args[i + 1]); i += 2; continue; }
|
|
if a == "-fps" && more >= 1 { options.fps = number(i + 1); i += 2; continue; }
|
|
if a == "-encoder" && more >= 1 { options.encoder = args[i + 1]; i += 2; continue; }
|
|
if a == "-out" && more >= 1 { options.path = args[i + 1]; i += 2; continue; }
|
|
if a == "-snapshot" && more >= 1 { snapshot_folder = args[i + 1]; i += 2; continue; }
|
|
if a == "-no-cursor" { options.cursor = false; i += 1; continue; }
|
|
if a == "-legacy" { options.legacy_duplicate = true; i += 1; continue; }
|
|
if a == "-cuda" { cuda = true; i += 1; continue; }
|
|
if a == "-exclude-test" { exclude_test = true; i += 1; continue; }
|
|
if a == "-dpi-unaware" { dpi_unaware = true; i += 1; continue; }
|
|
if a == "-animate" { animate = true; i += 1; continue; }
|
|
if a == "-border-test" { border_test = true; i += 1; continue; }
|
|
if a == "-keep-border" { options.testing |= .KEEP_BORDER; i += 1; continue; }
|
|
if a == "-simulate-unsupported" { options.testing |= .DXGI_UNSUPPORTED; i += 1; continue; }
|
|
if a == "-poll" { options.testing |= .WGC_POLL; i += 1; continue; }
|
|
if a == "-lose" { options.testing |= .LOSE_DUPLICATION; i += 1; continue; }
|
|
if a == "-sync-test" { sync_test = true; i += 1; continue; }
|
|
if a == "-mic-toggle" { mic_toggle = true; options.audio_track = true; i += 1; continue; }
|
|
if a == "-audio" && more >= 1 {
|
|
which := args[i + 1];
|
|
options.system_audio = which == "system" || which == "both";
|
|
options.microphone = which == "mic" || which == "both";
|
|
if !options.system_audio && !options.microphone { print("-audio takes system, mic or both\n"); exit(2); }
|
|
i += 2; continue;
|
|
}
|
|
if a == "-pause" && more >= 1 { pause_seconds = string_to_float64(args[i + 1]); i += 2; continue; }
|
|
if a == "-cursor-test" { cursor_test = true; i += 1; continue; }
|
|
if a == "-capture" && more >= 1 {
|
|
how := args[i + 1];
|
|
if how == "auto" options.capture = .AUTO;
|
|
else if how == "dxgi" options.capture = .DXGI;
|
|
else if how == "wgc" options.capture = .WGC;
|
|
else { print("-capture takes auto, dxgi or wgc\n"); exit(2); }
|
|
i += 2; continue;
|
|
}
|
|
if a == "-remux" && more >= 2 {
|
|
ok, error, packets := record_remux(args[i + 1], args[i + 2]);
|
|
if !ok { print("Cannot remux %: %\n", args[i + 1], error); exit(1); }
|
|
print("% packets into %\n", packets, args[i + 2]);
|
|
exit(0);
|
|
}
|
|
if a == "-check" && more >= 1 {
|
|
image, info, ok, error := video_poster(args[i + 1]);
|
|
if !ok { print("%: cannot be read: %\n", args[i + 1], error); exit(1); }
|
|
print("%: % x %, % s, %, % fps; a frame decoded at % s\n", args[i + 1], info.width, info.height, FF(info.duration, 2), info.codec, FF(info.frame_rate, 1), FF(info.poster_time, 2));
|
|
exit(0);
|
|
}
|
|
print("Unknown argument %\n", a);
|
|
exit(2);
|
|
}
|
|
|
|
if !dpi_unaware {
|
|
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 :: cast(HANDLE) -4;
|
|
if !SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) {
|
|
error := GetLastError();
|
|
ERROR_ACCESS_DENIED :: 5;
|
|
if error != ERROR_ACCESS_DENIED print("Could not become per-monitor DPI aware (v2): error %\n", error);
|
|
// Access denied: the awareness was set already, as it can be once only. dxgi_open and wgc_open
|
|
// refuse when the desktop they are told of is scaled.
|
|
}
|
|
}
|
|
print("DPI awareness: %\n", ifx dpi_unaware then "none" else "per monitor (v2), as mcap");
|
|
|
|
screens := record_list_screens();
|
|
print("Screens:\n");
|
|
for screens {
|
|
d := it.desktop;
|
|
print(" %: % on adapter % (%): % x % at %, %%\n", it_index, it.name, it.adapter_index, it.adapter,
|
|
d.right - d.left, d.bottom - d.top, d.left, d.top,
|
|
ifx it.rotation == .IDENTITY || it.rotation == .UNSPECIFIED then "" else tprint(", rotated %", it.rotation));
|
|
}
|
|
if list_only return;
|
|
|
|
if cuda {
|
|
ok, text := cuda_make_context();
|
|
print("CUDA: %\n", text);
|
|
if !ok exit(1);
|
|
}
|
|
|
|
border_screen := screens[ifx options.screen >= 0 then options.screen else 0].desktop;
|
|
border_before: s64;
|
|
if border_test border_before = yellow_edge_pixels(border_screen);
|
|
|
|
recorder: Recorder;
|
|
ok, error, fall_back := record_start(*recorder, options);
|
|
if !ok {
|
|
print("Cannot record: %\n", error);
|
|
if fall_back print("Desktop Duplication cannot run here: this is the case for the Windows.Graphics.Capture fallback.\n");
|
|
exit(ifx fall_back then cast(s32) 3 else 1);
|
|
}
|
|
stats := record_stats(*recorder);
|
|
if stats.fell_back_from print("Desktop Duplication could not run (%), so:\n", stats.fell_back_from);
|
|
print("Recording % x % for % s at up to % fps with % and %, to %\n", stats.width, stats.height, seconds, options.fps, stats.source, stats.encoder, options.path);
|
|
|
|
region := options.region;
|
|
if region.right <= region.left region = screens[ifx options.screen >= 0 then options.screen else 0].desktop;
|
|
excluded, control: HWND;
|
|
if exclude_test excluded, control = exclusion_windows(region);
|
|
saved_cursor: POINT;
|
|
cursor_at: POINT;
|
|
if cursor_test {
|
|
GetCursorPos(*saved_cursor);
|
|
cursor_at = .{region.left + (region.right - region.left) / 2, region.top + (region.bottom - region.top) / 2};
|
|
SetCursorPos(cursor_at.x, cursor_at.y);
|
|
}
|
|
flash: HWND;
|
|
beep: [] u8;
|
|
next_flash: float64 = 1;
|
|
if sync_test {
|
|
beep = beep_wav();
|
|
side := min(cast(s32) 200, (region.right - region.left) / 3, (region.bottom - region.top) / 3);
|
|
flash = square_window("record-probe flash", 0x00FFFFFF, region.left + (region.right - region.left - side) / 2, region.top + (region.bottom - region.top - side) / 2, side);
|
|
ShowWindow(flash, SW_HIDE);
|
|
}
|
|
mover: HWND;
|
|
if animate mover = square_window("record-probe animate", 0x0000A5FF, region.left, region.top, 120);
|
|
|
|
start := current_time_monotonic();
|
|
last_print := start;
|
|
border_during: s64 = -1;
|
|
while to_float64_seconds(current_time_monotonic() - start) < seconds {
|
|
pump_messages();
|
|
if mover { // back and forth across the region, about 240 times a second
|
|
elapsed := to_float64_seconds(current_time_monotonic() - start);
|
|
span := region.right - region.left - 120;
|
|
phase := fmod_cycling(elapsed * 0.5, 2.0);
|
|
x := region.left + cast(s32) (span * ifx phase < 1 then phase else 2 - phase);
|
|
SetWindowPos(mover, HWND_TOPMOST, x, region.top + (region.bottom - region.top) / 2 - 60, 0, 0, SWP_NOSIZE | SWP_NOACTIVATE);
|
|
sleep_milliseconds(4);
|
|
} else {
|
|
sleep_milliseconds(20);
|
|
}
|
|
if to_float64_seconds(current_time_monotonic() - last_print) >= 1 {
|
|
last_print = current_time_monotonic();
|
|
s := record_stats(*recorder);
|
|
print(" % s: % captured, % sent (% repeats), % written, % dropped, % lost, % KB\n",
|
|
FF(to_float64_seconds(last_print - start), 0), s.captured, s.emitted, s.repeats, s.written, s.dropped, s.lost, s.bytes / 1024);
|
|
}
|
|
if mic_toggle {
|
|
elapsed := to_float64_seconds(current_time_monotonic() - start);
|
|
should := elapsed >= 3 && elapsed < 7;
|
|
if should != record_microphone(*recorder) && (elapsed < 3.5 || (elapsed >= 7 && elapsed < 7.5)) {
|
|
record_set_microphone(*recorder, should);
|
|
print(" % s: microphone %\n", FF(elapsed, 1), ifx should then "on" else "off");
|
|
}
|
|
failed, why := record_microphone_failed(*recorder);
|
|
if failed print(" microphone failed: %\n", why);
|
|
}
|
|
if flash { // a flash and a beep together every 2 s, the flash for 200 ms
|
|
elapsed := to_float64_seconds(current_time_monotonic() - start);
|
|
if elapsed >= next_flash {
|
|
ShowWindow(flash, SW_SHOWNOACTIVATE);
|
|
PlaySoundW(beep.data, null, SND_MEMORY | SND_ASYNC);
|
|
next_flash += 2;
|
|
} else if elapsed >= next_flash - 2 + 0.2 {
|
|
ShowWindow(flash, SW_HIDE);
|
|
}
|
|
}
|
|
if pause_seconds > 0 {
|
|
elapsed := to_float64_seconds(current_time_monotonic() - start);
|
|
should := elapsed >= 2 && elapsed < 2 + pause_seconds;
|
|
if should != record_paused(*recorder) {
|
|
record_pause(*recorder, should);
|
|
print(" % s: %\n", FF(elapsed, 1), ifx should then "paused" else "resumed");
|
|
}
|
|
}
|
|
if border_test && border_during < 0 && to_float64_seconds(current_time_monotonic() - start) >= 1.5
|
|
border_during = yellow_edge_pixels(border_screen);
|
|
failed, _ := record_failed(*recorder);
|
|
if failed break;
|
|
}
|
|
|
|
last, width, height := record_last_frame(*recorder);
|
|
if mover DestroyWindow(mover);
|
|
if flash DestroyWindow(flash);
|
|
if cursor_test SetCursorPos(saved_cursor.x, saved_cursor.y);
|
|
stopped, stop_error := record_stop(*recorder);
|
|
s := recorder.stats;
|
|
print("\n%\n", ifx stopped then "Recorded." else tprint("Recording failed: %", stop_error));
|
|
ms_each :: (ticks: s64, count: s64) -> string #expand {
|
|
if !count return "-";
|
|
return tprint("% ms", FF(cast(float64) ticks / `recorder.frequency * 1000 / count, 2));
|
|
}
|
|
file_size := file_size_of(options.path);
|
|
print(" source: %\n", s.source);
|
|
print(" encoder: %\n", s.encoder);
|
|
print(" size: % x %\n", s.width, s.height);
|
|
print(" seconds: %\n", FF(s.seconds, 1));
|
|
print(" captured: % new images of the screen (% a second), % cursor-only changes\n", s.captured, FF(s.captured / s.seconds, 1), s.pointer_moves);
|
|
print(" sent: % frames (% a second), % of them repeats of an unchanged screen\n", s.emitted, FF(s.emitted / s.seconds, 1), s.repeats);
|
|
print(" written: %, dropped %, duplication lost %, paused % s\n", s.written, s.dropped, s.lost, FF(s.paused_seconds, 1));
|
|
print(" each: copy % (per image), read back % , encode % (per frame)\n",
|
|
ms_each(s.copy_ticks, s.captured), ms_each(s.readback_ticks, s.emitted), ms_each(s.encode_ticks, s.written));
|
|
print(" file: % KB, % MB a minute\n", file_size / 1024, FF(cast(float64) file_size / (1024 * 1024) / s.seconds * 60, 1));
|
|
if options.system_audio || options.microphone || options.audio_track {
|
|
print(" sound: %; % AAC frames%\n", ifx s.audio then s.audio else "none opened", s.audio_frames, ifx s.audio_errors then tprint(" (%)", s.audio_errors) else "");
|
|
sound := sound_report(options.path);
|
|
if sound.found print(" in file: % s of sound, % dB RMS, % onsets\n", FF(sound.seconds, 2), FF(sound.rms_db, 1), sound.onsets.count);
|
|
else print(" in file: no sound stream %\n", sound.error);
|
|
if sync_test && sound.found {
|
|
flashes := flash_times(options.path);
|
|
print(" sync: % flashes, % beeps\n", flashes.count, sound.onsets.count);
|
|
offsets: [..] float64;
|
|
offsets.allocator = temp;
|
|
for flash_time: flashes {
|
|
best: float64 = 1000000000.0;
|
|
for sound.onsets if abs(it - flash_time) < abs(best) best = it - flash_time;
|
|
if abs(best) < 0.5 array_add(*offsets, best);
|
|
}
|
|
if offsets.count {
|
|
text: [..] string; text.allocator = temp;
|
|
for offsets array_add(*text, tprint("%", FF(it * 1000, 0)));
|
|
print(" offsets: beep after flash, ms: %\n", join(..text, " ",, temp));
|
|
print(" drift: % ms from the first to the last\n", FF((offsets[offsets.count - 1] - offsets[0]) * 1000, 0));
|
|
}
|
|
}
|
|
}
|
|
|
|
if exclude_test {
|
|
excluded_rect := window_rect_in(excluded, region);
|
|
control_rect := window_rect_in(control, region);
|
|
excluded_seen := colour_fraction(last, width, height, excluded_rect, EXCLUDED_BGR);
|
|
control_seen := colour_fraction(last, width, height, control_rect, CONTROL_BGR);
|
|
print(" exclusion: the excluded square is % in the last frame, the control square %: %\n",
|
|
percent(excluded_seen), percent(control_seen),
|
|
ifx control_seen < 0.9 then "inconclusive (the control is not there)"
|
|
else ifx excluded_seen < 0.01 then "WDA_EXCLUDEFROMCAPTURE is respected"
|
|
else "WDA_EXCLUDEFROMCAPTURE is NOT respected");
|
|
DestroyWindow(excluded);
|
|
DestroyWindow(control);
|
|
}
|
|
|
|
if border_test {
|
|
print(" border: % yellow pixels along the screen's edges before recording, % while: %\n", border_before, border_during,
|
|
ifx border_during < 0 then "not measured (too short)"
|
|
else ifx border_during > border_before + 200 then "a capture border is drawn"
|
|
else "no capture border");
|
|
}
|
|
|
|
if snapshot_folder {
|
|
make_directory_if_it_does_not_exist(snapshot_folder, recursive = true);
|
|
rgba := bgra_to_rgba(last);
|
|
if cursor_test { // 48 x 48 around where the mouse was put, four times as large
|
|
SIDE :: 48; SCALE :: 4;
|
|
crop := NewArray(SIDE * SCALE * SIDE * SCALE * 4, u8,, temp);
|
|
cx, cy := cursor_at.x - region.left - 8, cursor_at.y - region.top - 8;
|
|
for y: 0..SIDE * SCALE - 1 for x: 0..SIDE * SCALE - 1 {
|
|
sx, sy := clamp(cx + x / SCALE, 0, width - 1), clamp(cy + y / SCALE, 0, height - 1);
|
|
memcpy(crop.data + (y * SIDE * SCALE + x) * 4, rgba.data + (cast(s64) sy * width + sx) * 4, 4);
|
|
}
|
|
cursor_path := tprint("%/cursor.png", snapshot_folder);
|
|
stbi_write_png(temp_c_string(cursor_path), SIDE * SCALE, SIDE * SCALE, 4, crop.data, SIDE * SCALE * 4);
|
|
print(" cursor: %\n", cursor_path);
|
|
}
|
|
captured_path := tprint("%/captured.png", snapshot_folder);
|
|
stbi_write_png(temp_c_string(captured_path), width, height, 4, rgba.data, width * 4);
|
|
image, info, decoded, decode_error := video_poster(options.path);
|
|
if decoded {
|
|
decoded_path := tprint("%/decoded.png", snapshot_folder);
|
|
stbi_write_png(temp_c_string(decoded_path), image.width, image.height, 4, image.pixels, image.width * 4);
|
|
print(" snapshot: % and % (decoded at % s of %: %, % fps)\n", captured_path, decoded_path, FF(info.poster_time, 2), FF(info.duration, 2), info.codec, FF(info.frame_rate, 1));
|
|
image_free(*image);
|
|
} else {
|
|
print(" snapshot: %; the file could not be decoded back: %\n", captured_path, decode_error);
|
|
}
|
|
}
|
|
exit(ifx stopped then cast(s32) 0 else 1);
|
|
}
|
|
|
|
#scope_file
|
|
|
|
EXCLUDED_BGR :: u8.[255, 0, 255]; // magenta
|
|
CONTROL_BGR :: u8.[255, 255, 0]; // cyan
|
|
|
|
// Two squares on the region, topmost: one kept out of captures, one not.
|
|
exclusion_windows :: (region: RECT) -> excluded: HWND, control: HWND {
|
|
width := region.right - region.left;
|
|
height := region.bottom - region.top;
|
|
side := min(cast(s32) 160, width / 4, height / 2);
|
|
y := region.top + (height - side) / 2;
|
|
excluded := square_window("record-probe excluded", 0x00FF00FF, region.left + width / 4 - side / 2, y, side);
|
|
control := square_window("record-probe control", 0x00FFFF00, region.left + 3 * width / 4 - side / 2, y, side);
|
|
WDA_EXCLUDEFROMCAPTURE :: 0x11;
|
|
if !SetWindowDisplayAffinity(excluded, WDA_EXCLUDEFROMCAPTURE) print("SetWindowDisplayAffinity failed; the test cannot say anything.\n");
|
|
pump_messages();
|
|
return excluded, control;
|
|
}
|
|
|
|
// A square of one colour, topmost, that never takes the focus.
|
|
square_window :: (class_name: string, colour: u32, x: s32, y: s32, side: s32) -> HWND {
|
|
instance := GetModuleHandleW(null);
|
|
wc: WNDCLASSEXW;
|
|
wc.cbSize = size_of(WNDCLASSEXW);
|
|
wc.lpfnWndProc = xx DefWindowProcW;
|
|
wc.hInstance = instance;
|
|
wc.hbrBackground = CreateSolidBrush(colour);
|
|
wc.lpszClassName = utf8_to_wide(class_name,, temp);
|
|
RegisterClassExW(*wc);
|
|
return CreateWindowExW(WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE, utf8_to_wide(class_name,, temp), utf8_to_wide(class_name,, temp),
|
|
WS_POPUP | WS_VISIBLE, x, y, side, side, null, null, instance, null);
|
|
}
|
|
|
|
// Pixels along a screen's outer 6 pixels that look like Windows' yellow capture border, with GDI.
|
|
yellow_edge_pixels :: (screen: RECT) -> s64 {
|
|
width, height := screen.right - screen.left, screen.bottom - screen.top;
|
|
desktop := GetDC(null);
|
|
defer ReleaseDC(null, desktop);
|
|
dc := CreateCompatibleDC(desktop);
|
|
defer DeleteDC(dc);
|
|
bitmap := CreateCompatibleBitmap(desktop, width, height);
|
|
defer DeleteObject(bitmap);
|
|
old := SelectObject(dc, bitmap);
|
|
BitBlt(dc, 0, 0, width, height, desktop, screen.left, screen.top, cast(ROP) (cast(u32) ROP.SRCCOPY | 0x40000000)); // CAPTUREBLT
|
|
SelectObject(dc, old);
|
|
|
|
info: BITMAPINFO;
|
|
info.bmiHeader.biSize = size_of(BITMAPINFOHEADER);
|
|
info.bmiHeader.biWidth = width;
|
|
info.bmiHeader.biHeight = -height;
|
|
info.bmiHeader.biPlanes = 1;
|
|
info.bmiHeader.biBitCount = 32;
|
|
info.bmiHeader.biCompression = BI_RGB;
|
|
pixels := NewArray(cast(s64) width * height * 4, u8,, temp);
|
|
GetDIBits(dc, bitmap, 0, xx height, pixels.data, *info, DIB_RGB_COLORS);
|
|
|
|
EDGE :: 6;
|
|
count: s64;
|
|
for y: 0..height - 1 for x: 0..width - 1 {
|
|
if x >= EDGE && x < width - EDGE && y >= EDGE && y < height - EDGE continue;
|
|
p := pixels.data + (cast(s64) y * width + x) * 4;
|
|
b, g, r := cast(s32) p[0], cast(s32) p[1], cast(s32) p[2];
|
|
if r > 200 && g > 120 && b < 90 && r - b > 130 count += 1;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
// A window's rectangle, in the region's pixels, a few pixels in from its edges.
|
|
window_rect_in :: (window: HWND, region: RECT) -> RECT {
|
|
r: RECT;
|
|
GetWindowRect(window, *r);
|
|
return .{r.left - region.left + 4, r.top - region.top + 4, r.right - region.left - 4, r.bottom - region.top - 4};
|
|
}
|
|
|
|
colour_fraction :: (pixels: [] u8, width: s32, height: s32, r: RECT, bgr: [3] u8) -> float64 {
|
|
total, same: s64;
|
|
for y: max(r.top, 0)..min(r.bottom, height) - 1 for x: max(r.left, 0)..min(r.right, width) - 1 {
|
|
p := pixels.data + (cast(s64) y * width + x) * 4;
|
|
total += 1;
|
|
if abs(cast(s32) p[0] - bgr[0]) <= 8 && abs(cast(s32) p[1] - bgr[1]) <= 8 && abs(cast(s32) p[2] - bgr[2]) <= 8 same += 1;
|
|
}
|
|
return ifx total then cast(float64) same / total else 0;
|
|
}
|
|
|
|
pump_messages :: () {
|
|
msg: MSG;
|
|
while PeekMessageW(*msg, null, 0, 0, PM_REMOVE) {
|
|
TranslateMessage(*msg);
|
|
DispatchMessageW(*msg);
|
|
}
|
|
}
|
|
|
|
// A CUDA context on the first device, as onnxruntime makes for OCR and labels in mcap, and kept.
|
|
cuda_make_context :: () -> ok: bool, text: string {
|
|
library := LoadLibraryW(utf8_to_wide("nvcuda.dll",, temp));
|
|
if !library return false, "nvcuda.dll is not installed";
|
|
Cu_Init :: #type (flags: u32) -> s32 #c_call;
|
|
Cu_Device :: #type (device: *s32, ordinal: s32) -> s32 #c_call;
|
|
Cu_Context :: #type (cuda_context: **void, flags: u32, device: s32) -> s32 #c_call;
|
|
Cu_Name :: #type (name: *u8, length: s32, device: s32) -> s32 #c_call;
|
|
cu_init := cast(Cu_Init) GetProcAddress(library, "cuInit");
|
|
cu_device_get := cast(Cu_Device) GetProcAddress(library, "cuDeviceGet");
|
|
cu_ctx_create := cast(Cu_Context) GetProcAddress(library, "cuCtxCreate_v2");
|
|
cu_name := cast(Cu_Name) GetProcAddress(library, "cuDeviceGetName");
|
|
if !cu_init || !cu_device_get || !cu_ctx_create || !cu_name return false, "nvcuda.dll lacks the calls needed";
|
|
result := cu_init(0);
|
|
if result return false, tprint("cuInit failed (%)", result);
|
|
device: s32;
|
|
result = cu_device_get(*device, 0);
|
|
if result return false, tprint("cuDeviceGet failed (%)", result);
|
|
name: [256] u8;
|
|
cu_name(name.data, name.count, device);
|
|
cuda_context: *void;
|
|
result = cu_ctx_create(*cuda_context, 0, device);
|
|
if result return false, tprint("cuCtxCreate failed (%)", result);
|
|
return true, tprint("a context on %, kept for the recording", to_string(name.data));
|
|
}
|
|
|
|
bgra_to_rgba :: (pixels: [] u8) -> [] u8 {
|
|
out := NewArray(pixels.count, u8,, temp);
|
|
for i: 0..pixels.count / 4 - 1 {
|
|
out[i * 4 + 0] = pixels[i * 4 + 2];
|
|
out[i * 4 + 1] = pixels[i * 4 + 1];
|
|
out[i * 4 + 2] = pixels[i * 4 + 0];
|
|
out[i * 4 + 3] = 255;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
file_size_of :: (path: string) -> s64 {
|
|
_, size, ok := file_modtime_and_size(path);
|
|
return ifx ok then size else 0;
|
|
}
|
|
|
|
percent :: (fraction: float64) -> string {
|
|
return join(tprint("%", FF(fraction * 100, 1)), "%",, temp);
|
|
}
|
|
|
|
FF :: (value: float64, decimals: s64) -> FormatFloat #expand {
|
|
return formatFloat(value, trailing_width = decimals, zero_removal = .NO);
|
|
}
|
|
|
|
SetProcessDpiAwarenessContext :: (value: HANDLE) -> BOOL #foreign user32_dll;
|
|
SetWindowDisplayAffinity :: (window: HWND, affinity: u32) -> BOOL #foreign user32_dll;
|
|
SetCursorPos :: (x: s32, y: s32) -> BOOL #foreign user32_dll;
|
|
|
|
// ---------------------------------------------------------------- sound and sync
|
|
|
|
// A beep of 1 kHz for 150 ms, as a WAV in memory, for PlaySound.
|
|
beep_wav :: () -> [] u8 {
|
|
RATE :: 48000;
|
|
COUNT :: RATE * 150 / 1000;
|
|
data := NewArray(44 + COUNT * 2, u8,, context.default_allocator);
|
|
put32 :: (at: s64, v: u32) #expand { for 0..3 `data[at + it] = cast,trunc(u8) (v >> (8 * it)); }
|
|
put16 :: (at: s64, v: u16) #expand { `data[at] = cast,trunc(u8) v; `data[at + 1] = cast,trunc(u8) (v >> 8); }
|
|
memcpy(data.data, "RIFF".data, 4); put32(4, cast(u32) (36 + COUNT * 2)); memcpy(data.data + 8, "WAVEfmt ".data, 8);
|
|
put32(16, 16); put16(20, 1); put16(22, 1); put32(24, RATE); put32(28, RATE * 2); put16(32, 2); put16(34, 16);
|
|
memcpy(data.data + 36, "data".data, 4); put32(40, COUNT * 2);
|
|
for i: 0..COUNT - 1 {
|
|
v := cast(s16) (sin(cast(float) i * 2 * PI * 1000 / RATE) * 20000);
|
|
put16(44 + i * 2, cast,no_check(u16) v);
|
|
}
|
|
return data;
|
|
}
|
|
|
|
// What the file's sound holds: its length, its level, and when it goes from quiet to loud (onsets).
|
|
Sound_Report :: struct { found: bool; seconds: float64; rms_db: float64; onsets: [..] float64; error: string; }
|
|
|
|
sound_report :: (path: string) -> Sound_Report {
|
|
report: Sound_Report;
|
|
report.onsets.allocator = temp;
|
|
format: *AVFormatContext;
|
|
if avformat_open_input(*format, temp_c_string(path), null, null) < 0 { report.error = "cannot open"; return report; }
|
|
defer avformat_close_input(*format);
|
|
avformat_find_stream_info(format, null);
|
|
index := -1;
|
|
for 0..format.nb_streams - 1 if format.streams[it].codecpar.codec_type == .AUDIO { index = it; break; }
|
|
if index < 0 return report;
|
|
report.found = true;
|
|
stream := format.streams[index];
|
|
codec := avcodec_find_decoder(stream.codecpar.codec_id);
|
|
c := avcodec_alloc_context3(codec);
|
|
defer avcodec_free_context(*c);
|
|
avcodec_parameters_to_context(c, stream.codecpar);
|
|
if avcodec_open2(c, codec, null) < 0 { report.error = "no decoder"; return report; }
|
|
packet := av_packet_alloc(); defer av_packet_free(*packet);
|
|
frame := av_frame_alloc(); defer av_frame_free(*frame);
|
|
|
|
WINDOW :: 480; // 10 ms at 48 kHz
|
|
sum_all: float64; count_all: s64;
|
|
window_sum: float64; window_count: s64; window_start: float64 = -1;
|
|
quiet_for: s64 = 100; // windows in a row below the threshold
|
|
first_time: float64 = -1; last_time: float64;
|
|
handle :: () #expand {
|
|
while avcodec_receive_frame(`c, `frame) >= 0 {
|
|
t := cast(float64) `frame.pts * `stream.time_base.num / `stream.time_base.den;
|
|
if `first_time < 0 `first_time = t;
|
|
left := cast(*float) `frame.data[0];
|
|
for i: 0..`frame.nb_samples - 1 {
|
|
v := cast(float64) left[i];
|
|
`sum_all += v * v; `count_all += 1;
|
|
if `window_start < 0 `window_start = t + cast(float64) i / 48000;
|
|
`window_sum += v * v; `window_count += 1;
|
|
if `window_count == WINDOW {
|
|
rms := sqrt(`window_sum / WINDOW);
|
|
if rms > 0.05 {
|
|
if `quiet_for >= 20 array_add(*`report.onsets, `window_start); // after 200 ms of quiet
|
|
`quiet_for = 0;
|
|
} else {
|
|
`quiet_for += 1;
|
|
}
|
|
`window_sum, `window_count, `window_start = 0, 0, -1;
|
|
}
|
|
}
|
|
`last_time = t + cast(float64) `frame.nb_samples / 48000;
|
|
}
|
|
}
|
|
while av_read_frame(format, packet) >= 0 {
|
|
if packet.stream_index == index { avcodec_send_packet(c, packet); handle(); }
|
|
av_packet_unref(packet);
|
|
}
|
|
avcodec_send_packet(c, null);
|
|
handle();
|
|
report.seconds = last_time - max(first_time, 0);
|
|
report.rms_db = ifx count_all && sum_all > 0 then 10 * log(sum_all / count_all) / log(10.0) else -200;
|
|
return report;
|
|
}
|
|
|
|
// When the video's middle turns white: the sync test's flashes, as times in seconds.
|
|
flash_times :: (path: string) -> [..] float64 {
|
|
times: [..] float64;
|
|
times.allocator = temp;
|
|
format: *AVFormatContext;
|
|
if avformat_open_input(*format, temp_c_string(path), null, null) < 0 return times;
|
|
defer avformat_close_input(*format);
|
|
avformat_find_stream_info(format, null);
|
|
index := -1;
|
|
for 0..format.nb_streams - 1 if format.streams[it].codecpar.codec_type == .VIDEO { index = it; break; }
|
|
if index < 0 return times;
|
|
stream := format.streams[index];
|
|
codec := avcodec_find_decoder(stream.codecpar.codec_id);
|
|
c := avcodec_alloc_context3(codec);
|
|
defer avcodec_free_context(*c);
|
|
avcodec_parameters_to_context(c, stream.codecpar);
|
|
if avcodec_open2(c, codec, null) < 0 return times;
|
|
packet := av_packet_alloc(); defer av_packet_free(*packet);
|
|
frame := av_frame_alloc(); defer av_frame_free(*frame);
|
|
was_white := false;
|
|
handle :: () #expand {
|
|
while avcodec_receive_frame(`c, `frame) >= 0 {
|
|
// The luma of the middle 32 x 32 (the first plane in every YUV format the encoders give).
|
|
w, h := `frame.width, `frame.height;
|
|
sum: s64;
|
|
for y: h / 2 - 16..h / 2 + 15 for x: w / 2 - 16..w / 2 + 15 sum += `frame.data[0][y * `frame.linesize[0] + x];
|
|
white := sum / 1024 > 200;
|
|
if white && !`was_white array_add(*`times, cast(float64) `frame.best_effort_timestamp * `stream.time_base.num / `stream.time_base.den);
|
|
`was_white = white;
|
|
}
|
|
}
|
|
while av_read_frame(format, packet) >= 0 {
|
|
if packet.stream_index == index { avcodec_send_packet(c, packet); handle(); }
|
|
av_packet_unref(packet);
|
|
}
|
|
avcodec_send_packet(c, null);
|
|
handle();
|
|
return times;
|
|
}
|
|
|
|
PlaySoundW :: (sound: *void, module: *void, flags: u32) -> s32 #foreign winmm_dll;
|
|
winmm_dll :: #library,system "winmm";
|
|
SND_ASYNC :: 0x1;
|
|
SND_MEMORY :: 0x4;
|
|
|
|
user32_dll :: #library,system "user32";
|
|
|
|
#import "Basic";
|
|
#import "String";
|
|
#import "File";
|
|
#import "File_Utilities";
|
|
#import "Math";
|
|
#import "Windows";
|
|
#import "Windows_Utf8";
|
|
#import "Screen_Record";
|
|
#import "Image_Decode";
|
|
#import "stb_image_write";
|
|
#import "ffmpeg";
|