feat: automatically record Steam game sessions
Detect games with native process exit watchers, own OBS on a background worker, and persist settings and recoverable session timelines. Place segments by split frame counts, recover capture with honest gaps, and finalise on game exit or explicit Quit. Keep the current recording alive after GPUI closes its last window, then exit when the game ends. Add an end-to-end rig covering recording, recovery, exact packet-count offsets, disk library reload and clean Quit.
This commit is contained in:
@@ -521,11 +521,15 @@ struct SignalContext {
|
||||
}
|
||||
|
||||
impl SignalContext {
|
||||
fn segment_started(&self, path: String) {
|
||||
fn segment_started(&self, path: String, total_frames: u64, dropped_frames: u64) {
|
||||
if let Ok(mut current) = self.current_segment.lock() {
|
||||
*current = Some(path.clone());
|
||||
}
|
||||
self.send(RecorderEvent::SegmentStarted { path });
|
||||
self.send(RecorderEvent::SegmentStarted {
|
||||
path,
|
||||
total_frames,
|
||||
dropped_frames,
|
||||
});
|
||||
}
|
||||
|
||||
fn send(&self, event: RecorderEvent) {
|
||||
@@ -549,7 +553,7 @@ unsafe extern "C" fn on_start(data: *mut c_void, _: *mut sys::calldata_t) {
|
||||
// outlives the connection.
|
||||
let context = unsafe { &*(data as *const SignalContext) };
|
||||
context.send(RecorderEvent::OutputStarted);
|
||||
context.segment_started(context.first_segment.clone());
|
||||
context.segment_started(context.first_segment.clone(), 0, 0);
|
||||
}
|
||||
|
||||
unsafe extern "C" fn on_stop(data: *mut c_void, calldata: *mut sys::calldata_t) {
|
||||
@@ -588,7 +592,17 @@ unsafe extern "C" fn on_file_changed(data: *mut c_void, calldata: *mut sys::call
|
||||
let settings = Data(unsafe { sys::obs_output_get_settings(context.output) });
|
||||
settings.set("format", &SettingValue::String(segment_stem(index + 1)));
|
||||
}
|
||||
context.segment_started(path);
|
||||
// SAFETY: the output outlives its signal connections.
|
||||
let total_frames = unsafe { sys::obs_output_get_total_frames(context.output) };
|
||||
// The split-triggering keyframe is already counted here, but belongs to
|
||||
// the new file. Consumers subtract one for placement; retain the raw count.
|
||||
// SAFETY: as above, the output outlives this callback.
|
||||
let dropped_frames = unsafe { sys::obs_output_get_frames_dropped(context.output) };
|
||||
context.segment_started(
|
||||
path,
|
||||
total_frames.max(0) as u64,
|
||||
dropped_frames.max(0) as u64,
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn segment_index(path: &str) -> Option<u64> {
|
||||
|
||||
@@ -112,7 +112,7 @@ impl Engine {
|
||||
microphone: config.microphone,
|
||||
};
|
||||
let output = OutputSpec {
|
||||
frames_per_second: self.frames_per_second,
|
||||
frames_per_second: config.frames_per_second.unwrap_or(self.frames_per_second),
|
||||
resolution: config.resolution,
|
||||
video_encoder: config.video_encoder,
|
||||
video_encoder_settings: vec![
|
||||
@@ -161,6 +161,8 @@ fn forward_slashes(path: &Path) -> String {
|
||||
}
|
||||
|
||||
pub struct RecordingConfig {
|
||||
/// Override the engine startup rate for this recording.
|
||||
pub frames_per_second: Option<u32>,
|
||||
pub video_source: Option<SourceSpec>,
|
||||
pub system_audio: Option<SourceSpec>,
|
||||
pub microphone: Option<SourceSpec>,
|
||||
@@ -282,7 +284,11 @@ pub enum RecorderEvent {
|
||||
/// OBS reports the output as started. Not yet proof that footage is written.
|
||||
OutputStarted,
|
||||
/// The muxer opened a new segment file; the previous one is complete.
|
||||
SegmentStarted { path: String },
|
||||
SegmentStarted {
|
||||
path: String,
|
||||
total_frames: u64,
|
||||
dropped_frames: u64,
|
||||
},
|
||||
OutputStopped {
|
||||
code: i64,
|
||||
last_error: Option<String>,
|
||||
|
||||
Reference in New Issue
Block a user