feat: detect lost sources, stalled writes and low disk space
PipeWire capture keeps its size and last frame after its stream stops, so the recorder follows the stream state OBS logs. Recording fails when the segment on disk stops growing (OBS kept counting bytes into a dead muxer and missed a full disk) and stops while enough space remains to finalise the last segment. Failures stay visible after the output stops. Adds a check command that verifies keyframe-aligned segments and a remux across segment boundaries, and gives the rig desktop audio.
This commit is contained in:
@@ -10,7 +10,7 @@ use std::{
|
||||
ptr,
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
atomic::{AtomicU8, AtomicU64, Ordering},
|
||||
mpsc::Sender,
|
||||
},
|
||||
};
|
||||
@@ -147,12 +147,38 @@ unsafe extern "C" fn log_to_stderr(
|
||||
_ => "debug",
|
||||
};
|
||||
if let Some(text) = owned(message.array) {
|
||||
if let Some(state) = pipewire_stream_state(&text) {
|
||||
let code = if state == "streaming" {
|
||||
STREAMING
|
||||
} else {
|
||||
NOT_STREAMING
|
||||
};
|
||||
PIPEWIRE_STREAM.store(code, Ordering::Relaxed);
|
||||
}
|
||||
eprintln!("obs {label}: {text}");
|
||||
}
|
||||
// SAFETY: frees the buffer dstr_vprintf allocated (null is allowed).
|
||||
unsafe { sys::bfree(message.array.cast()) };
|
||||
}
|
||||
|
||||
/// The state of the PipeWire capture stream, as last logged by OBS.
|
||||
///
|
||||
/// OBS 32.0.4's PipeWire capture keeps reporting its last size and rendering
|
||||
/// its last frame after the stream stops, and exposes the stream state only
|
||||
/// through this log line. Clipbin records one capture source at a time.
|
||||
static PIPEWIRE_STREAM: AtomicU8 = AtomicU8::new(NO_PIPEWIRE_STREAM);
|
||||
const NO_PIPEWIRE_STREAM: u8 = 0;
|
||||
const STREAMING: u8 = 1;
|
||||
const NOT_STREAMING: u8 = 2;
|
||||
|
||||
/// Parses `[pipewire] Stream 0x... state: "streaming" (error: none)` from
|
||||
/// linux-pipewire's on_state_changed_cb.
|
||||
fn pipewire_stream_state(message: &str) -> Option<&str> {
|
||||
let rest = message.strip_prefix("[pipewire] Stream ")?;
|
||||
let state = rest.split_once(" state: \"")?.1;
|
||||
state.split_once('"').map(|(state, _)| state)
|
||||
}
|
||||
|
||||
pub(crate) fn startup(spec: &StartupSpec) -> Result<Session, String> {
|
||||
// SAFETY: installs a handler with the signature libobs expects.
|
||||
unsafe { sys::base_set_log_handler(Some(log_to_stderr), ptr::null_mut()) };
|
||||
@@ -540,6 +566,7 @@ impl Pipeline {
|
||||
events: Sender<RecorderEvent>,
|
||||
current_segment: Arc<Mutex<Option<String>>>,
|
||||
) -> Result<Self, String> {
|
||||
PIPEWIRE_STREAM.store(NO_PIPEWIRE_STREAM, Ordering::Relaxed);
|
||||
// OBS releases objects asynchronously, so names must not be reused.
|
||||
static RECORDINGS: AtomicU64 = AtomicU64::new(0);
|
||||
let id = RECORDINGS.fetch_add(1, Ordering::Relaxed);
|
||||
@@ -716,9 +743,14 @@ impl Pipeline {
|
||||
sys::obs_source_get_height(self.video_source),
|
||||
)
|
||||
};
|
||||
let source_streaming = match PIPEWIRE_STREAM.load(Ordering::Relaxed) {
|
||||
NO_PIPEWIRE_STREAM => None,
|
||||
state => Some(state == STREAMING),
|
||||
};
|
||||
Health {
|
||||
source_width: width,
|
||||
source_height: height,
|
||||
source_streaming,
|
||||
output_active: sys::obs_output_active(self.output),
|
||||
total_frames: sys::obs_output_get_total_frames(self.output).max(0) as u64,
|
||||
dropped_frames: sys::obs_output_get_frames_dropped(self.output).max(0) as u64,
|
||||
@@ -726,6 +758,7 @@ impl Pipeline {
|
||||
lagged_frames: sys::obs_get_lagged_frames() as u64,
|
||||
current_segment: None,
|
||||
current_segment_bytes: 0,
|
||||
disk_available_bytes: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -810,6 +843,16 @@ unsafe fn create_source(spec: &SourceSpec, name: &CStr) -> Result<*mut sys::obs_
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pipewire_stream_state_is_read_from_the_plugin_log() {
|
||||
let line = r#"[pipewire] Stream 0x7fe128037440 state: "paused" (error: none)"#;
|
||||
assert_eq!(pipewire_stream_state(line), Some("paused"));
|
||||
assert_eq!(
|
||||
pipewire_stream_state("[pipewire] Screencast session created"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn segment_names_round_trip() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user