Skip to content

Commit ada2be5

Browse files
Merge pull request #2644 from CapSoftware/fix/cap-muxer-survives-recorder-crash
fix(cap-muxer): finish the open fragment when the recorder dies
2 parents fa92b85 + d394f61 commit ada2be5

4 files changed

Lines changed: 159 additions & 6 deletions

File tree

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/cap-muxer/Cargo.toml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,6 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
1818

1919
[lints]
2020
workspace = true
21+
22+
[dev-dependencies]
23+
tempfile = "3"

‎crates/cap-muxer/src/main.rs‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,37 +25,45 @@ fn main() -> ExitCode {
2525
match run() {
2626
Ok(()) => ExitCode::from(EXIT_OK),
2727
Err(MuxerError::Protocol(err)) => {
28-
eprintln!("cap-muxer protocol error: {err}");
28+
report(format_args!("cap-muxer protocol error: {err}"));
2929
ExitCode::from(EXIT_PROTOCOL_ERROR)
3030
}
3131
Err(MuxerError::DiskFull(err)) => {
32-
eprintln!("cap-muxer disk full: {err:#}");
32+
report(format_args!("cap-muxer disk full: {err:#}"));
3333
ExitCode::from(EXIT_DISK_FULL)
3434
}
3535
Err(MuxerError::FFmpeg(err)) => {
36-
eprintln!("cap-muxer ffmpeg error: {err:#}");
36+
report(format_args!("cap-muxer ffmpeg error: {err:#}"));
3737
ExitCode::from(EXIT_FFMPEG_ERROR)
3838
}
3939
Err(MuxerError::Init(err)) => {
40-
eprintln!("cap-muxer init error: {err:#}");
40+
report(format_args!("cap-muxer init error: {err:#}"));
4141
ExitCode::from(EXIT_INIT_ERROR)
4242
}
4343
Err(MuxerError::Abort(reason)) => {
44-
eprintln!("cap-muxer parent abort: {reason}");
44+
report(format_args!("cap-muxer parent abort: {reason}"));
4545
ExitCode::from(EXIT_ABORT)
4646
}
4747
Err(MuxerError::BadState(reason)) => {
48-
eprintln!("cap-muxer bad state: {reason}");
48+
report(format_args!("cap-muxer bad state: {reason}"));
4949
ExitCode::from(EXIT_BAD_STATE)
5050
}
5151
}
5252
}
5353

54+
/// Writes to stderr without panicking: when the recorder dies, the pipe it
55+
/// read our stderr through is closed, and the open fragment must still be
56+
/// finished after that.
57+
fn report(message: std::fmt::Arguments) {
58+
let _ = writeln!(io::stderr(), "{message}");
59+
}
60+
5461
fn init_tracing() {
5562
let filter = std::env::var("CAP_MUXER_LOG").unwrap_or_else(|_| "info".to_string());
5663
let _ = tracing_subscriber::fmt()
5764
.with_env_filter(filter)
5865
.with_writer(std::io::stderr)
66+
.log_internal_errors(false)
5967
.try_init();
6068
}
6169

‎crates/cap-muxer/tests/orphaned.rs‎

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
//! The recorder can die while cap-muxer runs. Its stdin then reaches EOF and
2+
//! the pipe the recorder read cap-muxer's stderr through is closed; cap-muxer
3+
//! must still finish the fragment it has open.
4+
5+
use std::{
6+
io::Write,
7+
process::{Command, Stdio},
8+
};
9+
10+
use cap_muxer_protocol::{
11+
Frame, InitVideo, PACKET_FLAG_KEYFRAME, Packet, STREAM_INDEX_VIDEO, StartParams, write_frame,
12+
write_packet,
13+
};
14+
use ffmpeg::{codec, format, frame};
15+
16+
const WIDTH: u32 = 320;
17+
const HEIGHT: u32 = 240;
18+
const FPS: i32 = 30;
19+
20+
type EncodedPacket = (i64, i64, bool, Vec<u8>);
21+
22+
fn encode(count: i64, gop: u32) -> (Vec<u8>, Vec<EncodedPacket>) {
23+
ffmpeg::init().unwrap();
24+
let codec = codec::encoder::find_by_name("libx264").expect("libx264");
25+
let mut context = codec::context::Context::new_with_codec(codec)
26+
.encoder()
27+
.video()
28+
.unwrap();
29+
context.set_width(WIDTH);
30+
context.set_height(HEIGHT);
31+
context.set_format(format::Pixel::YUV420P);
32+
context.set_time_base((1, FPS));
33+
context.set_frame_rate(Some((FPS, 1)));
34+
context.set_gop(gop);
35+
context.set_max_b_frames(0);
36+
context.set_flags(codec::Flags::GLOBAL_HEADER);
37+
let mut options = ffmpeg::Dictionary::new();
38+
options.set("preset", "ultrafast");
39+
options.set("tune", "zerolatency");
40+
let mut encoder = context.open_as_with(codec, options).unwrap();
41+
let extradata = unsafe {
42+
let ctx = encoder.as_ptr();
43+
std::slice::from_raw_parts((*ctx).extradata, (*ctx).extradata_size as usize).to_vec()
44+
};
45+
let mut packets = Vec::new();
46+
let mut packet = ffmpeg::Packet::empty();
47+
let mut drain = |encoder: &mut ffmpeg::encoder::Video, packets: &mut Vec<_>| {
48+
while encoder.receive_packet(&mut packet).is_ok() {
49+
packets.push((
50+
packet.pts().unwrap(),
51+
packet.dts().unwrap(),
52+
packet.is_key(),
53+
packet.data().unwrap().to_vec(),
54+
));
55+
}
56+
};
57+
for index in 0..count {
58+
let mut picture = frame::Video::new(format::Pixel::YUV420P, WIDTH, HEIGHT);
59+
for plane in 0..3 {
60+
picture.data_mut(plane).fill((index * 3 % 255) as u8);
61+
}
62+
picture.set_pts(Some(index));
63+
encoder.send_frame(&picture).unwrap();
64+
drain(&mut encoder, &mut packets);
65+
}
66+
encoder.send_eof().unwrap();
67+
drain(&mut encoder, &mut packets);
68+
(extradata, packets)
69+
}
70+
71+
#[test]
72+
fn an_orphaned_muxer_finishes_its_open_fragment() {
73+
let (extradata, packets) = encode(75, 30);
74+
let directory = tempfile::tempdir().unwrap();
75+
let output = directory.path().join("display");
76+
let mut child = Command::new(env!("CARGO_BIN_EXE_cap-muxer"))
77+
.env("CAP_MUXER_LOG", "warn")
78+
.stdin(Stdio::piped())
79+
.stdout(Stdio::null())
80+
.stderr(Stdio::piped())
81+
.spawn()
82+
.unwrap();
83+
let mut stdin = child.stdin.take().unwrap();
84+
write_frame(
85+
&mut stdin,
86+
&Frame::InitVideo(InitVideo {
87+
codec: "libx264".into(),
88+
width: WIDTH,
89+
height: HEIGHT,
90+
frame_rate_num: FPS,
91+
frame_rate_den: 1,
92+
time_base_num: 1,
93+
time_base_den: FPS,
94+
extradata,
95+
segment_duration_ms: 1000,
96+
}),
97+
)
98+
.unwrap();
99+
write_frame(
100+
&mut stdin,
101+
&Frame::Start(StartParams {
102+
output_directory: output.to_string_lossy().into_owned(),
103+
init_segment_name: "init.mp4".into(),
104+
media_segment_pattern: "segment_$Number%03d$.m4s".into(),
105+
}),
106+
)
107+
.unwrap();
108+
for (pts, dts, key, data) in &packets {
109+
write_packet(
110+
&mut stdin,
111+
&Packet {
112+
stream_index: STREAM_INDEX_VIDEO,
113+
pts: *pts,
114+
dts: *dts,
115+
duration: 1,
116+
flags: if *key { PACKET_FLAG_KEYFRAME } else { 0 },
117+
data,
118+
},
119+
)
120+
.unwrap();
121+
}
122+
stdin.flush().unwrap();
123+
// The recorder dies: its end of the stderr pipe closes, then stdin.
124+
drop(child.stderr.take());
125+
drop(stdin);
126+
let status = child.wait().unwrap();
127+
assert_eq!(status.code(), Some(0), "cap-muxer must finish, not panic");
128+
129+
let mut segments: Vec<_> = std::fs::read_dir(&output)
130+
.unwrap()
131+
.flatten()
132+
.map(|entry| entry.file_name().to_string_lossy().into_owned())
133+
.filter(|name| name.starts_with("segment_"))
134+
.collect();
135+
segments.sort();
136+
assert_eq!(
137+
segments,
138+
["segment_001.m4s", "segment_002.m4s", "segment_003.m4s"],
139+
"every fragment, including the one open when the recorder died, is finished"
140+
);
141+
}

0 commit comments

Comments
 (0)