- record_region, a new [global_hotkeys] action on Ctrl-PrintScreen (Windows only; none on macOS): starts a recording, or stops the one running. - The overlay's video mode (screen_capture_win32.jai): red outline and magnifier; the drag held to the screen it began on; under 240 x 180 (record_size_allowed, in Screen_Record) the outline is dashed, the label gives the minimum, and letting go takes nothing; a click or Enter takes the whole screen; no pixels copied. With immediate, no overlay at all: the screen under the cursor, for PrintScreen while recording. - src/app/screen_record.jai: records with modules/Screen_Record to %TEMP%/mcap at 30 fps; a recording that stops by itself keeps what it has, as does quitting (the shutdown step stops it). The MP4 goes into the library through the stills' jobs: a video .mcap in Screen Captures, the MP4 its original, a frame of it its rendered image, metadata.json as a still's, capture.json with Duration, FrameRate, VideoCodec, PosterTime, Encoder, Capture and DroppedFrames; the MP4 is deleted once it is in. The catalog reads a video's keys from capture.json too. - The capture thread makes itself aware of each monitor's DPI: ImGui makes only the main thread aware, and Desktop Duplication refused the app's capture thread with DXGI_ERROR_UNSUPPORTED (the fallback recorded with Windows.Graphics.Capture, and the log said so). - -record SECONDS records the screen under the cursor, puts it in the library and exits; -record-quit quits while recording instead. Both pass on a scratch library. - Tests: a "screen recording" group (the size rule; an MP4 from the module's encoder written as a recording reads back, and the catalog takes it as a video). The segment overlay test no longer depends on the colours chosen in the app's own data folder. - README, the format's README and the plan describe it.
512 lines
20 KiB
Plaintext
512 lines
20 KiB
Plaintext
// Screen_Record: a region of one screen recorded to H.264 in an MP4, for the mcap app's screen
|
|
// recording and tools/record_probe.jai (video-capture-plan.html). Windows only.
|
|
//
|
|
// Two threads. The capture thread takes the screen with DXGI Desktop Duplication (dxgi_source.jai),
|
|
// or with Windows.Graphics.Capture (wgc_source.jai) where duplication cannot run, each on a D3D11
|
|
// device of its own; copies the region on the GPU; reads it back through a ring of staging textures,
|
|
// so Map never waits on the GPU (capture_gpu.jai); and, for duplication, draws the cursor in
|
|
// (cursor.jai). The encoder thread hands the frames to FFmpeg (encoder.jai). Between them is a pool of
|
|
// RECORD_BUFFERS frames; when the encoder falls RECORD_QUEUE frames behind, the oldest waiting is
|
|
// dropped and counted.
|
|
//
|
|
// A frame is taken at most fps times a second, and only when the screen or the cursor changed, with
|
|
// one a second while nothing does, so players can seek. Each carries the time it was taken.
|
|
//
|
|
// recorder: Recorder;
|
|
// ok, error, fall_back := record_start(*recorder, .{path = "out.mp4", screen = 0});
|
|
// ...
|
|
// ok, error = record_stop(*recorder);
|
|
//
|
|
// With capture .AUTO (the default), Desktop Duplication is tried first and Windows.Graphics.Capture
|
|
// takes over when it cannot run: on a laptop with two GPUs, on a rotated screen, or with too many
|
|
// duplications of the screen running. fall_back is only returned for .DXGI, which does not fall back.
|
|
|
|
RECORD_MIN_WIDTH :: 240; // a smaller region is refused
|
|
RECORD_MIN_HEIGHT :: 180;
|
|
RECORD_QUEUE :: 8; // frames waiting for the encoder at most
|
|
RECORD_BUFFERS :: RECORD_QUEUE + 2; // and the one being filled, and the one being encoded
|
|
|
|
// Whether a region this size may be recorded: at least RECORD_MIN_WIDTH x RECORD_MIN_HEIGHT. It is
|
|
// rounded down to even sizes after, which keeps it at least that, as both are even.
|
|
record_size_allowed :: (width: s64, height: s64) -> bool {
|
|
return width >= RECORD_MIN_WIDTH && height >= RECORD_MIN_HEIGHT;
|
|
}
|
|
|
|
Record_Screen :: struct {
|
|
adapter_index, output_index: u32;
|
|
adapter: string; // "NVIDIA GeForce RTX 5080 Laptop GPU"
|
|
name: string; // "\\.\DISPLAY1"
|
|
desktop: RECT; // on the virtual desktop
|
|
rotation: DXGI_MODE_ROTATION;
|
|
monitor: HMONITOR;
|
|
}
|
|
|
|
Record_Capture :: enum u8 {
|
|
AUTO; // Desktop Duplication, else Windows.Graphics.Capture
|
|
DXGI; // Desktop Duplication only
|
|
WGC; // Windows.Graphics.Capture only
|
|
}
|
|
|
|
// For record-probe: paths a machine may not take by itself.
|
|
Record_Testing :: enum_flags u8 {
|
|
DXGI_UNSUPPORTED; // Desktop Duplication fails as on a laptop with two GPUs (DXGI_ERROR_UNSUPPORTED)
|
|
WGC_POLL; // Windows.Graphics.Capture polled, as when it will not take the FrameArrived handler
|
|
KEEP_BORDER; // Windows.Graphics.Capture's yellow border left on
|
|
}
|
|
|
|
Record_Options :: struct {
|
|
path: string; // the .mp4 written
|
|
screen: s32 = -1; // into record_list_screens(); -1: the one holding region
|
|
region: RECT; // on the virtual desktop; empty: the whole screen
|
|
fps: s32 = 30;
|
|
cursor := true;
|
|
encoder: string; // "" tries RECORD_ENCODERS in turn
|
|
capture: Record_Capture;
|
|
legacy_duplicate: bool; // the plain DuplicateOutput, not DuplicateOutput1: for record-probe
|
|
testing: Record_Testing;
|
|
}
|
|
|
|
Record_Stats :: struct {
|
|
source: string; // "DXGI DuplicateOutput1 on <adapter>", "Windows.Graphics.Capture on <adapter>, border off"
|
|
fell_back_from: string; // why Desktop Duplication could not run, when it fell back
|
|
encoder: string;
|
|
width, height: s32;
|
|
captured: s64; // new images of the screen
|
|
pointer_moves: s64; // cursor changes with no new image (Desktop Duplication)
|
|
emitted: s64; // frames sent to be encoded
|
|
repeats: s64; // of them, the once-a-second repeat of an unchanged screen
|
|
written: s64; // encoded
|
|
dropped: s64; // the encoder was too far behind
|
|
lost: s64; // times the duplication was lost and made again
|
|
copy_ticks, readback_ticks, encode_ticks: s64; // totals, in QueryPerformanceCounter ticks
|
|
bytes: s64; // encoded
|
|
seconds: float64; // from start to stop
|
|
}
|
|
|
|
Recorder :: struct {
|
|
options: Record_Options;
|
|
screen: Record_Screen;
|
|
local: RECT; // the region, in the screen's pixels
|
|
dxgi: Dxgi_Source; // the capture thread's: one of these is open
|
|
wgc: Wgc_Source;
|
|
using_wgc: bool;
|
|
encoder: Encoder;
|
|
width, height: s32;
|
|
frequency: s64; // QueryPerformanceCounter's
|
|
start_ticks: s64;
|
|
|
|
capture_thread, encoder_thread: Thread;
|
|
capture_started, encoder_started: bool;
|
|
running: bool;
|
|
stopping: bool; // set by record_stop; the capture thread ends, then the encoder
|
|
capture_ended: bool;
|
|
|
|
opened: Semaphore; // the capture thread has opened its source, or failed to
|
|
open_ok, open_fall_back: bool;
|
|
open_error: string;
|
|
|
|
mutex: Mutex;
|
|
ready: Semaphore; // signalled for each frame queued, and once when capture ends
|
|
frames: [RECORD_BUFFERS] Record_Frame;
|
|
free: [RECORD_BUFFERS] s32;
|
|
free_count: s32;
|
|
queue: [RECORD_BUFFERS] s32; // oldest first
|
|
queue_count: s32;
|
|
last_frame: [] u8; // the last frame queued, BGRA, for record_last_frame
|
|
stats: Record_Stats;
|
|
error: string; // why it stopped by itself; heap
|
|
}
|
|
|
|
Record_Frame :: struct {
|
|
pixels: [] u8; // BGRA, rows packed
|
|
ticks: s64;
|
|
}
|
|
|
|
// Opens the screen, the encoder and the file, and starts recording.
|
|
record_start :: (r: *Recorder, options: Record_Options) -> ok: bool, error: string, fall_back: bool {
|
|
r.* = .{};
|
|
r.options = options;
|
|
r.options.path = copy_string(options.path);
|
|
r.options.encoder = copy_string(options.encoder);
|
|
if r.options.fps <= 0 r.options.fps = 30;
|
|
|
|
screens := record_list_screens();
|
|
if !screens.count return false, "no screen to record", false;
|
|
screen_index := options.screen;
|
|
region := options.region;
|
|
has_region := region.right > region.left && region.bottom > region.top;
|
|
if screen_index < 0 {
|
|
if !has_region screen_index = 0;
|
|
else for screens if rect_inside(region, it.desktop) { screen_index = xx it_index; break; }
|
|
if screen_index < 0 return false, "the region is not on one screen", false;
|
|
}
|
|
if screen_index >= screens.count return false, tprint("there is no screen %", screen_index), false;
|
|
r.screen = screens[screen_index];
|
|
r.screen.adapter = copy_string(r.screen.adapter);
|
|
r.screen.name = copy_string(r.screen.name);
|
|
if !has_region region = r.screen.desktop;
|
|
if !rect_inside(region, r.screen.desktop) return false, "the region is not on that screen", false;
|
|
|
|
// The minimum first, then even both ways, in the screen's pixels: H.264's 4:2:0 colour needs it.
|
|
// RECORD_MIN_WIDTH and _HEIGHT are even, so what passes stays at least that size.
|
|
#assert RECORD_MIN_WIDTH % 2 == 0 && RECORD_MIN_HEIGHT % 2 == 0;
|
|
if !record_size_allowed(region.right - region.left, region.bottom - region.top)
|
|
return false, tprint("% x % is smaller than % x %", region.right - region.left, region.bottom - region.top, RECORD_MIN_WIDTH, RECORD_MIN_HEIGHT), false;
|
|
d := r.screen.desktop;
|
|
r.local = .{region.left - d.left, region.top - d.top, region.right - d.left, region.bottom - d.top};
|
|
r.local.right = r.local.left + ((r.local.right - r.local.left) & cast(s32) ~1);
|
|
r.local.bottom = r.local.top + ((r.local.bottom - r.local.top) & cast(s32) ~1);
|
|
r.width = r.local.right - r.local.left;
|
|
r.height = r.local.bottom - r.local.top;
|
|
r.stats.width, r.stats.height = r.width, r.height;
|
|
|
|
init(*r.mutex, "record");
|
|
init(*r.ready);
|
|
init(*r.opened);
|
|
for * r.frames it.pixels = NewArray(cast(s64) r.width * r.height * 4, u8, initialized = false);
|
|
for 0..RECORD_BUFFERS - 1 r.free[it] = cast(s32) it;
|
|
r.free_count = RECORD_BUFFERS;
|
|
r.last_frame = NewArray(cast(s64) r.width * r.height * 4, u8);
|
|
frequency: LARGE_INTEGER;
|
|
QueryPerformanceFrequency(*frequency);
|
|
r.frequency = frequency.QuadPart;
|
|
r.start_ticks = ticks_now();
|
|
|
|
// The source is opened on the capture thread, which uses it: WinRT's objects belong to the
|
|
// apartment they are made in.
|
|
thread_init(*r.capture_thread, record_capture_proc, name = "record capture");
|
|
r.capture_started = true;
|
|
thread_start(*r.capture_thread, r);
|
|
wait_for(*r.opened);
|
|
if !r.open_ok {
|
|
error, fall_back := copy_string(r.open_error,, temp), r.open_fall_back;
|
|
record_free(r);
|
|
return false, error, fall_back;
|
|
}
|
|
|
|
ok, error, tried := encoder_open(*r.encoder, r.options.path, r.width, r.height, r.options.fps, r.options.encoder);
|
|
if tried log("[record] Encoders that did not open: %", tried);
|
|
if !ok {
|
|
error = copy_string(error,, temp);
|
|
record_free(r);
|
|
return false, error, false;
|
|
}
|
|
r.stats.encoder = copy_string(r.encoder.name);
|
|
|
|
thread_init(*r.encoder_thread, record_encoder_proc, name = "record encoder");
|
|
r.encoder_started = true;
|
|
r.running = true;
|
|
thread_start(*r.encoder_thread, r);
|
|
return true, "", false;
|
|
}
|
|
|
|
// Stops, finishes the file and frees everything. ok is false when recording stopped by itself on an
|
|
// error, or the file could not be finished; what was recorded up to then is in the file all the same.
|
|
record_stop :: (r: *Recorder) -> ok: bool, error: string {
|
|
if !r.running return false, "not recording";
|
|
atomic_swap(*r.stopping, true);
|
|
thread_is_done(*r.capture_thread, -1);
|
|
thread_is_done(*r.encoder_thread, -1);
|
|
r.running = false;
|
|
|
|
closed, close_error := encoder_close(*r.encoder);
|
|
r.stats.seconds = cast(float64) (ticks_now() - r.start_ticks) / r.frequency;
|
|
r.stats.bytes = r.encoder.bytes;
|
|
error := ifx r.error then copy_string(r.error,, temp) else "";
|
|
record_free(r);
|
|
|
|
if error return false, error;
|
|
if !closed return false, close_error;
|
|
return true, "";
|
|
}
|
|
|
|
// As they are now; strings are the recorder's.
|
|
record_stats :: (r: *Recorder) -> Record_Stats {
|
|
if !r.running return r.stats;
|
|
lock(*r.mutex);
|
|
defer unlock(*r.mutex);
|
|
return r.stats;
|
|
}
|
|
|
|
// The last frame queued, BGRA (temp), cursor and all.
|
|
record_last_frame :: (r: *Recorder) -> pixels: [] u8, width: s32, height: s32 {
|
|
if r.running lock(*r.mutex);
|
|
pixels := array_copy(r.last_frame,, temp);
|
|
if r.running unlock(*r.mutex);
|
|
return pixels, r.width, r.height;
|
|
}
|
|
|
|
// Whether recording stopped by itself (the screen went away, the encoder failed); why, then.
|
|
record_failed :: (r: *Recorder) -> bool, string {
|
|
lock(*r.mutex);
|
|
defer unlock(*r.mutex);
|
|
return r.error.count > 0, r.error;
|
|
}
|
|
|
|
#scope_file
|
|
|
|
Staged :: struct { pending: bool; ticks: s64; pointer: POINT; pointer_visible: bool; }
|
|
|
|
// Stops the capture thread if it runs, and frees what record_start made. The stats' strings are kept.
|
|
record_free :: (r: *Recorder) {
|
|
atomic_swap(*r.stopping, true);
|
|
if r.capture_started { thread_is_done(*r.capture_thread, -1); thread_deinit(*r.capture_thread); r.capture_started = false; }
|
|
if r.encoder_started { thread_is_done(*r.encoder_thread, -1); thread_deinit(*r.encoder_thread); r.encoder_started = false; }
|
|
if r.encoder.format encoder_close(*r.encoder);
|
|
for * r.frames array_free(it.pixels);
|
|
array_free(r.last_frame);
|
|
r.last_frame = .[];
|
|
destroy(*r.opened);
|
|
destroy(*r.ready);
|
|
destroy(*r.mutex);
|
|
free(r.open_error);
|
|
free(r.error);
|
|
r.open_error, r.error = "", "";
|
|
}
|
|
|
|
// Desktop Duplication, unless asked for Windows.Graphics.Capture, which takes over when duplication
|
|
// cannot run here and the options allow.
|
|
record_open_source :: (r: *Recorder) -> bool {
|
|
if r.options.capture != .WGC {
|
|
ok, error, fall_back := dxgi_open(*r.dxgi, r.screen, r.local, r.options.legacy_duplicate);
|
|
if ok && r.options.testing & .DXGI_UNSUPPORTED {
|
|
ok, error, fall_back = false, "Desktop Duplication (testing): 0x887A0004 (DXGI_ERROR_UNSUPPORTED)", true;
|
|
}
|
|
if ok {
|
|
r.stats.source = sprint("DXGI % on %", r.dxgi.method, r.screen.adapter);
|
|
return true;
|
|
}
|
|
dxgi_close(*r.dxgi);
|
|
if r.options.capture == .DXGI || !fall_back {
|
|
r.open_error, r.open_fall_back = copy_string(error), fall_back;
|
|
return false;
|
|
}
|
|
log("[record] Desktop Duplication cannot run here (%); Windows.Graphics.Capture instead.", error);
|
|
r.stats.fell_back_from = copy_string(error);
|
|
}
|
|
ok, error := wgc_open(*r.wgc, r.screen, r.local, r.options.cursor, r.options.testing);
|
|
if !ok {
|
|
wgc_close(*r.wgc);
|
|
r.open_error = copy_string(error);
|
|
return false;
|
|
}
|
|
r.using_wgc = true;
|
|
r.stats.source = sprint("Windows.Graphics.Capture on %, %%", r.screen.adapter, r.wgc.border, ifx r.wgc.polling then ", polled" else "");
|
|
return true;
|
|
}
|
|
|
|
record_capture_proc :: (thread: *Thread) -> s64 {
|
|
r := cast(*Recorder) thread.data;
|
|
// Aware of each monitor's DPI, whatever the process is: Desktop Duplication refuses a thread that is
|
|
// not (DXGI_ERROR_UNSUPPORTED), and the mcap app's ImGui makes only its main thread aware.
|
|
gpu_thread_dpi_aware();
|
|
wgc_thread_start();
|
|
defer wgc_thread_end();
|
|
r.open_ok = record_open_source(r);
|
|
signal(*r.opened);
|
|
if !r.open_ok return 0;
|
|
gpu: *Capture_Gpu = ifx r.using_wgc then *r.wgc.gpu else *r.dxgi.gpu;
|
|
defer if r.using_wgc wgc_close(*r.wgc); else dxgi_close(*r.dxgi);
|
|
|
|
// A change is sent as soon as it comes once its turn is due: turns are a frame's time apart on a
|
|
// schedule that moves on a frame at a time, so the rate never passes fps, and a tenth of a frame
|
|
// early counts as on time, so a 60 Hz screen's frames, which jitter, all make a 60 fps cap.
|
|
period := r.frequency / r.options.fps;
|
|
early := period / 10;
|
|
turn := ticks_now();
|
|
last_emit: s64 = -r.frequency;
|
|
have_image, changed := false, false;
|
|
staged: [RECORD_STAGING] Staged;
|
|
emits: s64;
|
|
|
|
while !atomic_read(*r.stopping) {
|
|
now := ticks_now();
|
|
due := ifx changed then turn - early else last_emit + r.frequency; // the change, or the repeat
|
|
wait := cast(u32) clamp((due - now) * 1000 / r.frequency, 0, 100);
|
|
next: Capture_Next;
|
|
error: string;
|
|
if r.using_wgc next, error = wgc_next(*r.wgc, wait);
|
|
else next, error = dxgi_next(*r.dxgi, wait);
|
|
if next == {
|
|
case .FAILED;
|
|
record_fail(r, error);
|
|
break;
|
|
case .LOST;
|
|
// A mode change, the secure desktop, a game going fullscreen: duplicated again, and on.
|
|
lock(*r.mutex); r.stats.lost += 1; unlock(*r.mutex);
|
|
while !atomic_read(*r.stopping) {
|
|
hr := dxgi_duplicate(*r.dxgi);
|
|
if SUCCEEDED(hr) break;
|
|
if !hresult_is(hr, DXGI_ERROR_ACCESS_LOST) && !hresult_is(hr, 0x80070005) && !hresult_is(hr, DXGI_ERROR_NOT_CURRENTLY_AVAILABLE) {
|
|
record_fail(r, hresult_text("the screen could not be duplicated again", hr));
|
|
break;
|
|
}
|
|
sleep_milliseconds(250);
|
|
}
|
|
if atomic_read(*r.stopping) || r.error break;
|
|
continue;
|
|
case .IMAGE; #through;
|
|
case .IMAGE_AND_POINTER;
|
|
have_image, changed = true, true;
|
|
lock(*r.mutex);
|
|
r.stats.captured += 1;
|
|
r.stats.copy_ticks = gpu.copy_ticks;
|
|
unlock(*r.mutex);
|
|
case .POINTER;
|
|
if r.options.cursor && have_image {
|
|
changed = true;
|
|
lock(*r.mutex); r.stats.pointer_moves += 1; unlock(*r.mutex);
|
|
}
|
|
}
|
|
|
|
now = ticks_now();
|
|
if !have_image continue;
|
|
repeat := !changed && now - last_emit >= r.frequency;
|
|
if !(changed && now >= turn - early) && !repeat continue;
|
|
turn = max(turn + period, now - period); // after a quiet spell, no burst to catch up
|
|
|
|
// Copied to staging now; read back two frames on, when the GPU is done with it.
|
|
slot := emits % RECORD_STAGING;
|
|
if staged[slot].pending record_read_back(r, gpu, slot, staged[slot]);
|
|
ID3D11DeviceContext_CopyResource(gpu.device_context, gpu.staging[slot], gpu.latest);
|
|
staged[slot] = .{pending = true, ticks = now};
|
|
if !r.using_wgc staged[slot].pointer, staged[slot].pointer_visible = r.dxgi.pointer_position, r.dxgi.pointer_visible;
|
|
emits += 1;
|
|
last_emit = now;
|
|
changed = false;
|
|
lock(*r.mutex);
|
|
r.stats.emitted += 1;
|
|
if repeat r.stats.repeats += 1;
|
|
unlock(*r.mutex);
|
|
}
|
|
|
|
// What is still staged, oldest first.
|
|
for 0..RECORD_STAGING - 1 {
|
|
slot := (emits + it) % RECORD_STAGING;
|
|
if staged[slot].pending record_read_back(r, gpu, slot, staged[slot]);
|
|
}
|
|
atomic_swap(*r.capture_ended, true);
|
|
signal(*r.ready);
|
|
return 0;
|
|
}
|
|
|
|
// Copies a staging texture into a free frame, draws the cursor (Desktop Duplication leaves it out)
|
|
// and queues it.
|
|
record_read_back :: (r: *Recorder, gpu: *Capture_Gpu, slot: s64, staged: Staged) {
|
|
t := ticks_now();
|
|
lock(*r.mutex);
|
|
if !r.free_count { // every frame is waiting: drop the oldest
|
|
r.free[r.free_count] = r.queue[0];
|
|
r.free_count += 1;
|
|
for 1..r.queue_count - 1 r.queue[it - 1] = r.queue[it];
|
|
r.queue_count -= 1;
|
|
r.stats.dropped += 1;
|
|
}
|
|
r.free_count -= 1;
|
|
index := r.free[r.free_count];
|
|
unlock(*r.mutex);
|
|
frame := *r.frames[index];
|
|
|
|
mapped: D3D11_MAPPED_SUBRESOURCE;
|
|
hr := ID3D11DeviceContext_Map(gpu.device_context, gpu.staging[slot], 0, .READ, 0, *mapped);
|
|
if FAILED(hr) {
|
|
lock(*r.mutex); r.free[r.free_count] = index; r.free_count += 1; unlock(*r.mutex);
|
|
record_fail(r, hresult_text("reading the frame back", hr));
|
|
return;
|
|
}
|
|
row := cast(s64) r.width * 4;
|
|
for y: 0..r.height - 1 memcpy(frame.pixels.data + y * row, cast(*u8) mapped.pData + cast(s64) y * mapped.RowPitch, row);
|
|
ID3D11DeviceContext_Unmap(gpu.device_context, gpu.staging[slot], 0);
|
|
if r.options.cursor && staged.pointer_visible {
|
|
cursor_draw(frame.pixels.data, r.width, r.height, *r.dxgi.shape,
|
|
staged.pointer.x - cast(s32) gpu.box.left, staged.pointer.y - cast(s32) gpu.box.top);
|
|
}
|
|
frame.ticks = staged.ticks;
|
|
|
|
lock(*r.mutex);
|
|
r.queue[r.queue_count] = index;
|
|
r.queue_count += 1;
|
|
memcpy(r.last_frame.data, frame.pixels.data, frame.pixels.count);
|
|
r.stats.readback_ticks += ticks_now() - t;
|
|
unlock(*r.mutex);
|
|
signal(*r.ready);
|
|
}
|
|
|
|
record_encoder_proc :: (thread: *Thread) -> s64 {
|
|
r := cast(*Recorder) thread.data;
|
|
first: s64 = -1;
|
|
while true {
|
|
wait_for(*r.ready, 100);
|
|
lock(*r.mutex);
|
|
index: s32 = -1;
|
|
if r.queue_count {
|
|
index = r.queue[0];
|
|
for 1..r.queue_count - 1 r.queue[it - 1] = r.queue[it];
|
|
r.queue_count -= 1;
|
|
}
|
|
unlock(*r.mutex);
|
|
if index < 0 {
|
|
if atomic_read(*r.capture_ended) break;
|
|
continue;
|
|
}
|
|
|
|
frame := *r.frames[index];
|
|
if first < 0 first = frame.ticks;
|
|
pts := (frame.ticks - first) * 90000 / r.frequency;
|
|
t := ticks_now();
|
|
ok, error := encoder_write(*r.encoder, frame.pixels.data, pts);
|
|
|
|
lock(*r.mutex);
|
|
r.free[r.free_count] = index;
|
|
r.free_count += 1;
|
|
r.stats.encode_ticks += ticks_now() - t;
|
|
if ok r.stats.written += 1;
|
|
r.stats.bytes = r.encoder.bytes;
|
|
unlock(*r.mutex);
|
|
if !ok {
|
|
record_fail(r, error);
|
|
// Frames still come until the capture thread sees it; they are let go.
|
|
while !atomic_read(*r.capture_ended) {
|
|
wait_for(*r.ready, 100);
|
|
lock(*r.mutex);
|
|
for 0..r.queue_count - 1 { r.free[r.free_count] = r.queue[it]; r.free_count += 1; }
|
|
r.queue_count = 0;
|
|
unlock(*r.mutex);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Ends the recording from one of its threads; the first reason is kept.
|
|
record_fail :: (r: *Recorder, error: string) {
|
|
log_error("[record] %", error);
|
|
lock(*r.mutex);
|
|
if !r.error r.error = copy_string(error);
|
|
unlock(*r.mutex);
|
|
atomic_swap(*r.stopping, true);
|
|
}
|
|
|
|
rect_inside :: (inner: RECT, outer: RECT) -> bool {
|
|
return inner.left >= outer.left && inner.top >= outer.top && inner.right <= outer.right && inner.bottom <= outer.bottom;
|
|
}
|
|
|
|
#scope_export
|
|
|
|
#load "capture_gpu.jai";
|
|
#load "dxgi_source.jai";
|
|
#load "wgc_source.jai";
|
|
#load "encoder.jai";
|
|
#load "cursor.jai";
|
|
|
|
#import "Basic";
|
|
#import "String";
|
|
#import "Atomics";
|
|
#import "Thread";
|
|
#import "Windows";
|
|
#import "Windows_Utf8";
|
|
#import "dxgi";
|
|
#import "d3d11";
|
|
#import "ffmpeg";
|