alpha2_ffmpeg_bridge/
log.rs1use std::collections::VecDeque;
9use std::ffi::{CStr, c_char, c_int};
10use std::sync::{Mutex, Once};
11
12const RING_LINES: usize = 256;
13const DETAIL_LINES: usize = 4;
14const DETAIL_CHARS: usize = 200;
15
16#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct LogLine {
19 pub seq: u64,
21 pub level: i32,
23 pub class: String,
25 pub text: String,
27}
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub struct LogMark(u64);
32
33struct Ring {
34 next: u64,
35 lines: VecDeque<LogLine>,
36}
37
38static RING: Mutex<Ring> = Mutex::new(Ring {
39 next: 0,
40 lines: VecDeque::new(),
41});
42
43unsafe extern "C" {
44 fn alpha2_log_install();
45}
46
47pub fn install_log_capture() {
49 static INSTALL: Once = Once::new();
50 INSTALL.call_once(|| unsafe { alpha2_log_install() });
52}
53
54#[must_use]
56pub fn log_mark() -> LogMark {
57 install_log_capture();
58 LogMark(ring().next)
59}
60
61#[must_use]
64pub fn log_since(mark: LogMark, classes: &[&str]) -> Vec<LogLine> {
65 ring()
66 .lines
67 .iter()
68 .filter(|line| line.seq >= mark.0)
69 .filter(|line| classes.is_empty() || classes.contains(&line.class.as_str()))
70 .cloned()
71 .collect()
72}
73
74#[must_use]
77pub fn log_detail(lines: &[LogLine]) -> String {
78 if lines.is_empty() {
79 return String::new();
80 }
81 let quoted = lines[lines.len().saturating_sub(DETAIL_LINES)..]
82 .iter()
83 .map(|line| {
84 let text = ascii(line.text.trim())
85 .chars()
86 .take(DETAIL_CHARS)
87 .collect::<String>();
88 if line.class.is_empty() {
89 text
90 } else {
91 format!("[{}] {text}", ascii(&line.class))
92 }
93 })
94 .collect::<Vec<_>>();
95 format!("; FFmpeg said: {}", quoted.join(" | "))
96}
97
98fn ascii(text: &str) -> String {
99 text.chars()
100 .map(|c| {
101 if c == ' ' || c.is_ascii_graphic() {
102 c.to_string()
103 } else {
104 c.escape_unicode().to_string()
105 }
106 })
107 .collect()
108}
109
110fn ring() -> std::sync::MutexGuard<'static, Ring> {
111 RING.lock()
113 .unwrap_or_else(std::sync::PoisonError::into_inner)
114}
115
116fn record(level: i32, class: String, text: &str) {
117 let mut ring = ring();
118 let seq = ring.next;
119 ring.next += 1;
120 if ring.lines.len() == RING_LINES {
121 ring.lines.pop_front();
122 }
123 ring.lines.push_back(LogLine {
124 seq,
125 level,
126 class,
127 text: text.trim_end_matches(['\n', '\r']).to_owned(),
128 });
129}
130
131#[unsafe(no_mangle)]
136unsafe extern "C" fn alpha2_log_record(
137 level: c_int,
138 class_name: *const c_char,
139 line: *const c_char,
140) {
141 let text = |pointer: *const c_char| {
142 if pointer.is_null() {
143 String::new()
144 } else {
145 unsafe { CStr::from_ptr(pointer) }
147 .to_string_lossy()
148 .into_owned()
149 }
150 };
151 let _ = std::panic::catch_unwind(|| record(level, text(class_name), &text(line)));
153}
154
155#[cfg(test)]
156pub(crate) fn record_for_tests(level: i32, class: &str, text: &str) {
157 record(level, class.to_owned(), text);
158}