- 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.
332 lines
15 KiB
Plaintext
332 lines
15 KiB
Plaintext
// While recording (app/screen_record.jai), on Windows: a thin red border just outside the region, and a
|
||
// toolbar beside it with the time, the size, frames dropped, and Mic, Pause, Stop and Discard. Both are
|
||
// plain GDI windows, as the overlay is, so they work while mcap is hidden in the tray, and both are
|
||
// kept out of every capture (WDA_EXCLUDEFROMCAPTURE), the recording's own included. The border is
|
||
// outside the region anyway, so it could never be in the video.
|
||
//
|
||
// Neither takes the focus: the window being recorded keeps it. The border lets clicks through. A
|
||
// toolbar button only asks (record_ui.asked); screen_record_frame does it, outside the window's own
|
||
// message, as Stop and Discard close the toolbar. Discard asks first, in the toolbar itself: a
|
||
// message box would be in the recording.
|
||
|
||
Record_UI_Ask :: enum u8 { NONE; MICROPHONE; PAUSE; STOP; DISCARD; }
|
||
|
||
record_ui: struct {
|
||
border, toolbar: HWND;
|
||
classes_registered: bool;
|
||
scale: float;
|
||
font: HFONT;
|
||
asked: Record_UI_Ask; // a button pressed, for screen_record_frame
|
||
confirming: bool; // Discard pressed once: "Discard it? Yes No"
|
||
hovered: s32 = -1; // the button under the mouse
|
||
buttons: [4] Record_UI_Button;
|
||
button_count: s32;
|
||
started: Apollo_Time;
|
||
size_text: string; // "1920 × 1080"; heap
|
||
}
|
||
|
||
Record_UI_Button :: struct { rect: RECT; label: string; ask: Record_UI_Ask; confirm: bool; }
|
||
|
||
// Shows the border around region (on the virtual desktop) and the toolbar beside it.
|
||
record_ui_open :: (region: RECT, whole_screen: bool, width: s32, height: s32) {
|
||
record_ui_close();
|
||
instance := GetModuleHandleW(null);
|
||
if !record_ui.classes_registered {
|
||
wc: WNDCLASSEXW;
|
||
wc.cbSize = size_of(WNDCLASSEXW);
|
||
wc.lpfnWndProc = xx DefWindowProcW;
|
||
wc.hInstance = instance;
|
||
wc.hbrBackground = CreateSolidBrush(RECORD_UI_RED);
|
||
wc.lpszClassName = utf8_to_wide(RECORD_BORDER_CLASS,, context.default_allocator);
|
||
RegisterClassExW(*wc);
|
||
wc.lpfnWndProc = xx record_toolbar_proc;
|
||
wc.hbrBackground = null;
|
||
wc.hCursor = LoadCursorW(null, IDC_ARROW);
|
||
wc.lpszClassName = utf8_to_wide(RECORD_TOOLBAR_CLASS,, context.default_allocator);
|
||
RegisterClassExW(*wc);
|
||
record_ui.classes_registered = true;
|
||
}
|
||
record_ui.started = current_time_monotonic();
|
||
record_ui.asked = .NONE;
|
||
record_ui.confirming = false;
|
||
record_ui.hovered = -1;
|
||
free(record_ui.size_text,, context.default_allocator);
|
||
record_ui.size_text = sprint("% × %", width, height,, context.default_allocator);
|
||
|
||
monitor := MonitorFromPoint(.{region.left, region.top}, MONITOR_DEFAULTTONEAREST);
|
||
info: MONITORINFO;
|
||
info.cbSize = size_of(MONITORINFO);
|
||
GetMonitorInfoW(monitor, *info);
|
||
screen := info.rcMonitor;
|
||
|
||
// The toolbar first, to know the screen's DPI.
|
||
ex_style :: WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE;
|
||
record_ui.toolbar = CreateWindowExW(ex_style, utf8_to_wide(RECORD_TOOLBAR_CLASS,, temp), utf8_to_wide("Recording",, temp),
|
||
WS_POPUP, screen.left, screen.top, 10, 10, null, null, instance, null);
|
||
if !record_ui.toolbar return;
|
||
dpi := GetDpiForWindow(record_ui.toolbar);
|
||
record_ui.scale = ifx dpi then dpi / 96.0 else 1.0;
|
||
font_height := cast(s32) (14 * record_ui.scale + 0.5);
|
||
record_ui.font = CreateFontW(-font_height, 0, 0, 0, 600, 0, 0, 0, 1, 0, 0, CLEARTYPE_QUALITY, 0, utf8_to_wide("Segoe UI",, temp));
|
||
|
||
// Below the region, at its right; else above; else inside its top right (kept out of the recording).
|
||
toolbar_width, toolbar_height := record_toolbar_size();
|
||
border := record_ui_px(3);
|
||
gap := record_ui_px(6);
|
||
x := region.right - toolbar_width;
|
||
y := region.bottom + border + gap;
|
||
if y + toolbar_height > screen.bottom y = region.top - border - gap - toolbar_height;
|
||
if y < screen.top || whole_screen {
|
||
y = region.top + gap;
|
||
x = region.right - toolbar_width - gap;
|
||
}
|
||
x = clamp(x, screen.left, screen.right - toolbar_width);
|
||
SetWindowPos(record_ui.toolbar, HWND_TOPMOST, x, y, toolbar_width, toolbar_height, SWP_NOACTIVATE);
|
||
visible := array_find(app.args, "-record-ui-visible"); // to look at them in a screenshot
|
||
if !visible SetWindowDisplayAffinity(record_ui.toolbar, WDA_EXCLUDEFROMCAPTURE);
|
||
ShowWindow(record_ui.toolbar, SW_SHOWNOACTIVATE);
|
||
SetTimer(record_ui.toolbar, RECORD_UI_TIMER, 250, null);
|
||
|
||
// The border: a frame border pixels wide just outside the region, which lets clicks through. Not
|
||
// for a whole screen, where it would be on the screen beside it.
|
||
if !whole_screen {
|
||
outer := RECT.{region.left - border, region.top - border, region.right + border, region.bottom + border};
|
||
record_ui.border = CreateWindowExW(ex_style | WS_EX_LAYERED | WS_EX_TRANSPARENT, utf8_to_wide(RECORD_BORDER_CLASS,, temp), utf8_to_wide("Recording",, temp),
|
||
WS_POPUP, outer.left, outer.top, outer.right - outer.left, outer.bottom - outer.top, null, null, instance, null);
|
||
if record_ui.border {
|
||
SetLayeredWindowAttributes(record_ui.border, 0, 255, LWA_ALPHA);
|
||
frame := CreateRectRgn(0, 0, outer.right - outer.left, outer.bottom - outer.top);
|
||
inside := CreateRectRgn(border, border, outer.right - outer.left - border, outer.bottom - outer.top - border);
|
||
CombineRgn(frame, frame, inside, RGN_DIFF);
|
||
DeleteObject(inside);
|
||
SetWindowRgn(record_ui.border, frame, .TRUE); // the window owns the region from here
|
||
if !visible SetWindowDisplayAffinity(record_ui.border, WDA_EXCLUDEFROMCAPTURE);
|
||
ShowWindow(record_ui.border, SW_SHOWNOACTIVATE);
|
||
}
|
||
}
|
||
}
|
||
|
||
record_ui_close :: () {
|
||
if record_ui.toolbar { KillTimer(record_ui.toolbar, RECORD_UI_TIMER); DestroyWindow(record_ui.toolbar); record_ui.toolbar = null; }
|
||
if record_ui.border { DestroyWindow(record_ui.border); record_ui.border = null; }
|
||
if record_ui.font { DeleteObject(record_ui.font); record_ui.font = null; }
|
||
}
|
||
|
||
// From another thread (the shutdown's): hidden, not destroyed, which only the main thread may do.
|
||
record_ui_hide :: () {
|
||
if record_ui.toolbar ShowWindowAsync(record_ui.toolbar, SW_HIDE);
|
||
if record_ui.border ShowWindowAsync(record_ui.border, SW_HIDE);
|
||
}
|
||
|
||
// The button pressed since the last call, once.
|
||
record_ui_take_ask :: () -> Record_UI_Ask {
|
||
ask := record_ui.asked;
|
||
record_ui.asked = .NONE;
|
||
return ask;
|
||
}
|
||
|
||
record_ui_refresh :: () {
|
||
if record_ui.toolbar InvalidateRect(record_ui.toolbar, null, .FALSE);
|
||
}
|
||
|
||
#scope_file
|
||
|
||
RECORD_BORDER_CLASS :: "mcapRecordBorder";
|
||
RECORD_TOOLBAR_CLASS :: "mcapRecordToolbar";
|
||
RECORD_UI_TIMER :: 1;
|
||
RECORD_UI_RED :: 0x002828E8; // COLORREF 0x00BBGGRR, as the overlay's video mode
|
||
RECORD_UI_GREY :: 0x00909090;
|
||
RECORD_UI_AMBER :: 0x0000B4FF;
|
||
RECORD_UI_BACK :: 0x00202020;
|
||
RECORD_UI_BUTTON :: 0x00383838;
|
||
RECORD_UI_HOVER :: 0x00505050;
|
||
RECORD_UI_TEXT :: 0x00F0F0F0;
|
||
|
||
record_ui_px :: (pixels: float) -> s32 {
|
||
return max(cast(s32) (pixels * record_ui.scale + 0.5), 1);
|
||
}
|
||
|
||
// What the toolbar shows now, left to right: the status, then the buttons.
|
||
record_toolbar_texts :: () -> status: string, dropped: string {
|
||
stats := record_stats(*screen_record.recorder);
|
||
paused := record_paused(*screen_record.recorder);
|
||
seconds := max(to_float64_seconds(current_time_monotonic() - record_ui.started) - stats.paused_seconds, 0.0);
|
||
whole := cast(s64) seconds;
|
||
two :: (n: s64) -> string { return tprint("%", formatInt(n, minimum_digits = 2)); }
|
||
time := ifx whole >= 3600 then tprint("%:%:%", whole / 3600, two(whole / 60 % 60), two(whole % 60)) else tprint("%:%", two(whole / 60), two(whole % 60));
|
||
status := tprint("% % %", ifx paused then "PAUSED" else "REC", time, record_ui.size_text);
|
||
dropped := ifx stats.dropped then tprint("% dropped", stats.dropped) else "";
|
||
if stats.lost {
|
||
lost := tprint("screen lost % time%", stats.lost, ifx stats.lost == 1 then "" else "s");
|
||
dropped = ifx dropped then tprint("%, %", dropped, lost) else lost;
|
||
}
|
||
return status, dropped;
|
||
}
|
||
|
||
// The buttons for the state the toolbar is in.
|
||
record_toolbar_buttons :: () -> [] Record_UI_Button {
|
||
if record_ui.confirming {
|
||
record_ui.buttons[0] = .{label = "Yes, discard", ask = .DISCARD};
|
||
record_ui.buttons[1] = .{label = "No", confirm = true};
|
||
record_ui.button_count = 2;
|
||
} else {
|
||
record_ui.buttons[0] = .{label = ifx record_microphone(*screen_record.recorder) then "Mic: on" else "Mic: off", ask = .MICROPHONE};
|
||
record_ui.buttons[1] = .{label = ifx record_paused(*screen_record.recorder) then "Resume" else "Pause", ask = .PAUSE};
|
||
record_ui.buttons[2] = .{label = "Stop", ask = .STOP};
|
||
record_ui.buttons[3] = .{label = "Discard", confirm = true};
|
||
record_ui.button_count = 4;
|
||
}
|
||
return array_view(record_ui.buttons, 0, record_ui.button_count);
|
||
}
|
||
|
||
text_width :: (dc: HDC, text: string) -> s32 {
|
||
wide := utf8_to_wide(text,, temp);
|
||
count: s32;
|
||
while wide[count] count += 1;
|
||
size: SIZE;
|
||
GetTextExtentPoint32W(dc, wide, count, *size);
|
||
return size.cx;
|
||
}
|
||
|
||
// The toolbar's size, and where its buttons are, for the longest it is likely to be.
|
||
record_toolbar_size :: () -> width: s32, height: s32 {
|
||
dc := GetDC(record_ui.toolbar);
|
||
defer ReleaseDC(record_ui.toolbar, dc);
|
||
old := SelectObject(dc, record_ui.font);
|
||
defer SelectObject(dc, old);
|
||
padding, gap := record_ui_px(10), record_ui_px(8);
|
||
status_width := text_width(dc, tprint("PAUSED 00:00:00 %", record_ui.size_text));
|
||
dropped_width := text_width(dc, "999 dropped, screen lost 9 times");
|
||
buttons_width := text_width(dc, "Mic: off") + text_width(dc, "Resume") + text_width(dc, "Stop") + text_width(dc, "Yes, discard") + 4 * 2 * padding + 3 * gap;
|
||
height := record_ui_px(34);
|
||
width := padding + record_ui_px(12) + gap + status_width + gap + dropped_width + gap + buttons_width + padding;
|
||
return width, height;
|
||
}
|
||
|
||
record_toolbar_proc :: (hwnd: HWND, message: u32, wParam: WPARAM, lParam: LPARAM) -> LRESULT #c_call {
|
||
if !platform.main_context return DefWindowProcW(hwnd, message, wParam, lParam);
|
||
push_context platform.main_context.* {
|
||
if message == {
|
||
case WM_MOUSEACTIVATE;
|
||
return MA_NOACTIVATE; // the window being recorded keeps the focus
|
||
case WM_ERASEBKGND;
|
||
return 1;
|
||
case WM_TIMER;
|
||
// The time moves on, and a mouse that left the toolbar no longer lights a button.
|
||
cursor: POINT;
|
||
GetCursorPos(*cursor);
|
||
window: RECT;
|
||
GetWindowRect(hwnd, *window);
|
||
if cursor.x < window.left || cursor.x >= window.right || cursor.y < window.top || cursor.y >= window.bottom record_ui.hovered = -1;
|
||
InvalidateRect(hwnd, null, .FALSE);
|
||
return 0;
|
||
case WM_MOUSEMOVE;
|
||
hovered := record_toolbar_hit(.{cast(s32) LOWORD(lParam), cast(s32) HIWORD(lParam)});
|
||
if hovered != record_ui.hovered { record_ui.hovered = hovered; InvalidateRect(hwnd, null, .FALSE); }
|
||
return 0;
|
||
case WM_LBUTTONDOWN;
|
||
hit := record_toolbar_hit(.{cast(s32) LOWORD(lParam), cast(s32) HIWORD(lParam)});
|
||
if hit >= 0 {
|
||
button := record_ui.buttons[hit];
|
||
if button.confirm record_ui.confirming = !record_ui.confirming;
|
||
else if button.ask != .NONE { record_ui.asked = button.ask; record_ui.confirming = false; }
|
||
InvalidateRect(hwnd, null, .FALSE);
|
||
}
|
||
return 0;
|
||
case WM_PAINT;
|
||
record_toolbar_paint(hwnd);
|
||
return 0;
|
||
}
|
||
return DefWindowProcW(hwnd, message, wParam, lParam);
|
||
}
|
||
}
|
||
|
||
record_toolbar_hit :: (p: POINT) -> s32 {
|
||
for array_view(record_ui.buttons, 0, record_ui.button_count) {
|
||
r := it.rect;
|
||
if p.x >= r.left && p.x < r.right && p.y >= r.top && p.y < r.bottom return cast(s32) it_index;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
record_toolbar_paint :: (hwnd: HWND) {
|
||
paint: PAINTSTRUCT;
|
||
window_dc := BeginPaint(hwnd, *paint);
|
||
defer EndPaint(hwnd, *paint);
|
||
client: RECT;
|
||
GetClientRect(hwnd, *client);
|
||
width, height := client.right, client.bottom;
|
||
|
||
// Drawn whole into a bitmap, then put on screen: no flicker as the time changes.
|
||
dc := CreateCompatibleDC(window_dc);
|
||
defer DeleteDC(dc);
|
||
bitmap := CreateCompatibleBitmap(window_dc, width, height);
|
||
defer DeleteObject(bitmap);
|
||
old_bitmap := SelectObject(dc, bitmap);
|
||
defer SelectObject(dc, old_bitmap);
|
||
old_font := SelectObject(dc, record_ui.font);
|
||
defer SelectObject(dc, old_font);
|
||
|
||
fill :: (r: RECT, colour: u32) #expand {
|
||
brush := CreateSolidBrush(colour);
|
||
rect := r;
|
||
FillRect(`dc, *rect, brush);
|
||
DeleteObject(brush);
|
||
}
|
||
text_at :: (x: s32, text: string, colour: u32) #expand {
|
||
SetTextColor(`dc, colour);
|
||
wide := utf8_to_wide(text,, temp);
|
||
count: s32;
|
||
while wide[count] count += 1;
|
||
size: SIZE;
|
||
GetTextExtentPoint32W(`dc, wide, count, *size);
|
||
TextOutW(`dc, x, (`height - size.cy) / 2, wide, count);
|
||
}
|
||
|
||
fill(.{0, 0, width, height}, RECORD_UI_BACK);
|
||
SetBkMode(dc, TRANSPARENT);
|
||
padding, gap := record_ui_px(10), record_ui_px(8);
|
||
paused := record_paused(*screen_record.recorder);
|
||
|
||
// The dot: red while recording, blinking each half second; grey while paused.
|
||
dot := record_ui_px(12);
|
||
blink := (cast(s64) (to_float64_seconds(current_time_monotonic() - record_ui.started) * 2)) % 2 == 0;
|
||
dot_colour: u32 = ifx paused then cast(u32) RECORD_UI_GREY else ifx blink then cast(u32) RECORD_UI_RED else cast(u32) 0x00602020;
|
||
brush := CreateSolidBrush(dot_colour);
|
||
old_brush := SelectObject(dc, brush);
|
||
old_pen := SelectObject(dc, GetStockObject(NULL_PEN));
|
||
Ellipse(dc, padding, (height - dot) / 2, padding + dot, (height - dot) / 2 + dot);
|
||
SelectObject(dc, old_pen);
|
||
SelectObject(dc, old_brush);
|
||
DeleteObject(brush);
|
||
|
||
status, dropped := record_toolbar_texts();
|
||
x := padding + dot + gap;
|
||
text_at(x, status, RECORD_UI_TEXT);
|
||
x += text_width(dc, status) + gap;
|
||
if dropped text_at(x, dropped, RECORD_UI_AMBER);
|
||
|
||
// The buttons, from the right.
|
||
buttons := record_toolbar_buttons();
|
||
button_height := height - 2 * record_ui_px(5);
|
||
right := width - padding;
|
||
for < buttons {
|
||
label_width := text_width(dc, it.label);
|
||
r := RECT.{right - label_width - 2 * padding, (height - button_height) / 2, right, (height - button_height) / 2 + button_height};
|
||
record_ui.buttons[it_index].rect = r;
|
||
lit := it.ask == .MICROPHONE && record_microphone(*screen_record.recorder); // on: in the record red
|
||
fill(r, ifx lit then cast(u32) RECORD_UI_RED else ifx it_index == record_ui.hovered then cast(u32) RECORD_UI_HOVER else cast(u32) RECORD_UI_BUTTON);
|
||
SetTextColor(dc, ifx it.ask == .DISCARD then cast(u32) RECORD_UI_AMBER else cast(u32) RECORD_UI_TEXT);
|
||
wide := utf8_to_wide(it.label,, temp);
|
||
count: s32;
|
||
while wide[count] count += 1;
|
||
size: SIZE;
|
||
GetTextExtentPoint32W(dc, wide, count, *size);
|
||
TextOutW(dc, r.left + padding, r.top + (button_height - size.cy) / 2, wide, count);
|
||
right = r.left - gap;
|
||
}
|
||
if record_ui.confirming text_at(right - text_width(dc, "Discard it?"), "Discard it?", RECORD_UI_AMBER);
|
||
|
||
BitBlt(window_dc, 0, 0, width, height, dc, 0, 0, .SRCCOPY);
|
||
}
|