Skip to content

Commit 708b62f

Browse files
committed
fix(core): 统一音频时间轴并加固重采样安全边界
1 parent b1ae653 commit 708b62f

8 files changed

Lines changed: 215 additions & 44 deletions

File tree

crates/ffmpeg_audio/src/core/format.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,45 @@
11
use crate::sys;
22

3+
mod private {
4+
pub trait Sealed {}
5+
}
6+
37
/// A trait binding Rust native numeric types to FFmpeg's `AVSampleFormat`.
48
///
59
/// This trait is used to ensure type safety when extracting resampled audio data.
6-
pub trait AudioSample: Copy + Send + Sync + 'static {
10+
///
11+
/// This trait is sealed because the resampler reinterprets bytes written by FFmpeg as `Self`.
12+
/// Allowing downstream implementations would make it possible to violate Rust's layout,
13+
/// alignment, or valid-bit-pattern requirements through an otherwise safe API.
14+
pub trait AudioSample: private::Sealed + Copy + Send + Sync + 'static {
715
/// The FFmpeg sample format enum corresponding to the packed (interleaved) layout.
816
const PACKED_FORMAT: sys::AVSampleFormat;
917

1018
/// The FFmpeg sample format enum corresponding to the planar layout.
1119
const PLANAR_FORMAT: sys::AVSampleFormat;
1220
}
1321

22+
impl private::Sealed for f32 {}
1423
impl AudioSample for f32 {
1524
const PACKED_FORMAT: sys::AVSampleFormat = sys::AVSampleFormat_AV_SAMPLE_FMT_FLT;
1625
const PLANAR_FORMAT: sys::AVSampleFormat = sys::AVSampleFormat_AV_SAMPLE_FMT_FLTP;
1726
}
1827

28+
impl private::Sealed for i16 {}
1929
impl AudioSample for i16 {
2030
const PACKED_FORMAT: sys::AVSampleFormat = sys::AVSampleFormat_AV_SAMPLE_FMT_S16;
2131
const PLANAR_FORMAT: sys::AVSampleFormat = sys::AVSampleFormat_AV_SAMPLE_FMT_S16P;
2232
}
2333

34+
impl private::Sealed for i32 {}
2435
impl AudioSample for i32 {
2536
#[expect(clippy::use_self)]
2637
const PACKED_FORMAT: sys::AVSampleFormat = sys::AVSampleFormat_AV_SAMPLE_FMT_S32;
2738
#[expect(clippy::use_self)]
2839
const PLANAR_FORMAT: sys::AVSampleFormat = sys::AVSampleFormat_AV_SAMPLE_FMT_S32P;
2940
}
3041

42+
impl private::Sealed for u8 {}
3143
impl AudioSample for u8 {
3244
const PACKED_FORMAT: sys::AVSampleFormat = sys::AVSampleFormat_AV_SAMPLE_FMT_U8;
3345
const PLANAR_FORMAT: sys::AVSampleFormat = sys::AVSampleFormat_AV_SAMPLE_FMT_U8P;

crates/ffmpeg_audio/src/core/frame.rs

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use crate::{
1616
pub struct AudioFrame<'a> {
1717
ptr: NonNull<sys::AVFrame>,
1818
time_base: TimeBase,
19+
timeline_origin_pts: i64,
1920
sample_offset: usize,
2021
_marker: PhantomData<&'a mut ()>,
2122
}
@@ -31,6 +32,7 @@ impl AudioFrame<'_> {
3132
Self {
3233
ptr: NonNull::new(ptr.cast_mut()).expect("FFmpeg returned a null AVFrame pointer"),
3334
time_base,
35+
timeline_origin_pts: 0,
3436
sample_offset: 0,
3537
_marker: PhantomData,
3638
}
@@ -42,6 +44,12 @@ impl AudioFrame<'_> {
4244
self
4345
}
4446

47+
/// Defines the stream timestamp that corresponds to the public timeline origin.
48+
pub(crate) const fn with_timeline_origin(mut self, origin_pts: i64) -> Self {
49+
self.timeline_origin_pts = origin_pts;
50+
self
51+
}
52+
4553
/// Extracts the underlying raw FFmpeg `AVFrame` pointer.
4654
///
4755
/// This is used internally to pass the raw frame data into FFmpeg's FFI functions
@@ -80,11 +88,15 @@ impl AudioFrame<'_> {
8088
unsafe { (*self.ptr.as_ptr()).sample_rate }
8189
}
8290

83-
/// Returns the Presentation Timestamp (PTS) in microseconds, if available.
91+
/// Returns the PTS in microseconds relative to the stream timeline origin, if available.
8492
pub(crate) fn pts_micros(&self) -> Option<i64> {
8593
let raw_pts = unsafe { (*self.ptr.as_ptr()).pts };
94+
if raw_pts == sys::AV_NOPTS_VALUE {
95+
return None;
96+
}
97+
let relative_pts = raw_pts.saturating_sub(self.timeline_origin_pts);
8698

87-
self.time_base.calc_micros(raw_pts).map(|mut micros| {
99+
self.time_base.calc_micros(relative_pts).map(|mut micros| {
88100
let sample_rate = self.frame_sample_rate();
89101

90102
if self.sample_offset > 0 && sample_rate > 0 {
@@ -96,7 +108,8 @@ impl AudioFrame<'_> {
96108
})
97109
}
98110

99-
/// Returns the Presentation Timestamp (PTS) of this frame, if available.
111+
/// Returns the Presentation Timestamp (PTS) of this frame relative to the stream timeline
112+
/// origin, if available.
100113
///
101114
/// The timestamp is automatically adjusted forward by the internal sample offset.
102115
///
@@ -106,15 +119,58 @@ impl AudioFrame<'_> {
106119
#[must_use]
107120
pub fn pts(&self) -> Option<Duration> {
108121
let raw_pts = unsafe { (*self.ptr.as_ptr()).pts };
122+
if raw_pts == sys::AV_NOPTS_VALUE {
123+
return None;
124+
}
125+
let relative_pts = raw_pts.saturating_sub(self.timeline_origin_pts);
126+
127+
self.time_base
128+
.calc_duration(relative_pts)
129+
.map(|mut duration| {
130+
let sample_rate = self.frame_sample_rate();
131+
132+
if self.sample_offset > 0 && sample_rate > 0 {
133+
let offset_secs = self.sample_offset as f64 / f64::from(sample_rate);
134+
duration += Duration::from_secs_f64(offset_secs);
135+
}
136+
duration
137+
})
138+
}
139+
}
109140

110-
self.time_base.calc_duration(raw_pts).map(|mut duration| {
111-
let sample_rate = self.frame_sample_rate();
112-
113-
if self.sample_offset > 0 && sample_rate > 0 {
114-
let offset_secs = self.sample_offset as f64 / f64::from(sample_rate);
115-
duration += Duration::from_secs_f64(offset_secs);
116-
}
117-
duration
141+
#[cfg(test)]
142+
mod tests {
143+
use std::{
144+
mem,
145+
time::Duration,
146+
};
147+
148+
use super::*;
149+
150+
#[test]
151+
fn pts_is_relative_to_the_timeline_origin() {
152+
let mut raw_frame = unsafe { mem::zeroed::<sys::AVFrame>() };
153+
raw_frame.pts = 10_000_000;
154+
raw_frame.nb_samples = 1_024;
155+
raw_frame.sample_rate = 48_000;
156+
157+
let time_base = TimeBase::try_new(sys::AVRational {
158+
num: 1,
159+
den: 1_000_000,
118160
})
161+
.unwrap();
162+
163+
let frame = AudioFrame::new(&raw const raw_frame, time_base)
164+
.with_timeline_origin(10_000_000)
165+
.with_offset(48);
166+
167+
assert_eq!(frame.pts_micros(), Some(1_000));
168+
assert_eq!(frame.pts(), Some(Duration::from_micros(1_000)));
169+
170+
raw_frame.pts = sys::AV_NOPTS_VALUE;
171+
let no_pts_frame =
172+
AudioFrame::new(&raw const raw_frame, time_base).with_timeline_origin(-1);
173+
assert_eq!(no_pts_frame.pts_micros(), None);
174+
assert_eq!(no_pts_frame.pts(), None);
119175
}
120176
}

crates/ffmpeg_audio/src/decode/decoder.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ impl Decoder {
6464
self.frame
6565
}
6666

67+
pub const fn is_flushing(&self) -> bool {
68+
self.is_flushing
69+
}
70+
6771
pub fn send_packet(&mut self, packet: *const sys::AVPacket) -> Result<()> {
6872
unsafe {
6973
sys::avcodec_send_packet(self.ctx, packet).into_ff_result()?;
@@ -82,10 +86,12 @@ impl Decoder {
8286
if self.is_flushing {
8387
return Ok(());
8488
}
85-
self.is_flushing = true;
89+
8690
unsafe {
8791
sys::avcodec_send_packet(self.ctx, ptr::null()).into_ff_result()?;
8892
}
93+
94+
self.is_flushing = true;
8995
Ok(())
9096
}
9197

crates/ffmpeg_audio/src/decode/demuxer.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,19 @@ impl Demuxer {
127127
}
128128
}
129129

130+
/// Returns the raw stream PTS corresponding to the public timeline origin.
131+
pub fn timeline_origin_pts(&self) -> i64 {
132+
unsafe {
133+
let stream_ptr = *(*self.ctx).streams.add(self.audio_stream_idx);
134+
let start_time = (*stream_ptr).start_time;
135+
if start_time == sys::AV_NOPTS_VALUE {
136+
0
137+
} else {
138+
start_time
139+
}
140+
}
141+
}
142+
130143
pub fn seek_to(&mut self, target: Duration) -> Result<()> {
131144
unsafe {
132145
let stream_ptr = *(*self.ctx).streams.add(self.audio_stream_idx);
@@ -136,10 +149,7 @@ impl Demuxer {
136149

137150
let mut pts = sys::av_rescale_q(target_us, sys::MICROSECONDS_Q, time_base);
138151

139-
let start_time = (*stream_ptr).start_time;
140-
if start_time != sys::AV_NOPTS_VALUE {
141-
pts = pts.saturating_add(start_time);
142-
}
152+
pts = pts.saturating_add(self.timeline_origin_pts());
143153

144154
let min_pts = i64::MIN;
145155
let max_pts = pts;

crates/ffmpeg_audio/src/decode/engine.rs

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ pub struct DecodeEngine {
5454
/// The fundamental unit of time representation for the current stream.
5555
time_base: TimeBase,
5656

57+
/// Raw stream PTS that maps to the public zero-based timeline.
58+
timeline_origin_pts: i64,
59+
5760
/// The presentation timestamp (PTS) of the most recently decoded frame, if available.
5861
current_pts: Option<Duration>,
5962

@@ -89,11 +92,13 @@ impl DecodeEngine {
8992
let decoder = Decoder::new(codec_params)?;
9093

9194
let time_base = demuxer.time_base()?;
95+
let timeline_origin_pts = demuxer.timeline_origin_pts();
9296

9397
Ok(Self {
9498
demuxer,
9599
decoder,
96100
time_base,
101+
timeline_origin_pts,
97102
current_pts: None,
98103
is_exhausted: false,
99104
has_buffered_seek_frame: false,
@@ -130,8 +135,9 @@ impl DecodeEngine {
130135
if self.has_buffered_seek_frame {
131136
self.has_buffered_seek_frame = false;
132137
let frame_ptr = self.decoder.current_frame();
133-
let audio_frame =
134-
AudioFrame::new(frame_ptr, self.time_base).with_offset(self.buffered_seek_offset);
138+
let audio_frame = AudioFrame::new(frame_ptr, self.time_base)
139+
.with_timeline_origin(self.timeline_origin_pts)
140+
.with_offset(self.buffered_seek_offset);
135141

136142
self.buffered_seek_offset = 0;
137143
self.current_pts = audio_frame.pts();
@@ -143,16 +149,26 @@ impl DecodeEngine {
143149
loop {
144150
match self.decoder.receive_frame() {
145151
Ok(Some(frame)) => {
146-
let audio_frame = AudioFrame::new(frame, self.time_base);
152+
let audio_frame = AudioFrame::new(frame, self.time_base)
153+
.with_timeline_origin(self.timeline_origin_pts);
147154
self.current_pts = audio_frame.pts();
148155

149156
self.debug_verify();
150157
return Ok(Some(audio_frame));
151158
}
152-
Err(AudioError::Eagain) => match self.demuxer.read_packet()? {
153-
Some(packet) => self.decoder.send_packet(packet)?,
154-
None => self.decoder.send_eof_flush()?,
155-
},
159+
Err(AudioError::Eagain) => {
160+
if let Some(packet) = self.demuxer.read_packet()? {
161+
self.decoder.send_packet(packet)?;
162+
} else {
163+
if self.decoder.is_flushing() {
164+
self.is_exhausted = true;
165+
self.debug_verify();
166+
return Ok(None);
167+
}
168+
169+
self.decoder.send_eof_flush()?;
170+
}
171+
}
156172
Ok(None) => {
157173
self.is_exhausted = true;
158174
self.debug_verify();
@@ -234,6 +250,37 @@ impl DecodeEngine {
234250
Ok(())
235251
}
236252

253+
/// Returns the position from which the next `receive_frame` call should resume.
254+
fn next_read_position(&self) -> Duration {
255+
if self.has_buffered_seek_frame {
256+
let frame_ptr = self.decoder.current_frame();
257+
return AudioFrame::new(frame_ptr, self.time_base)
258+
.with_timeline_origin(self.timeline_origin_pts)
259+
.with_offset(self.buffered_seek_offset)
260+
.pts()
261+
.unwrap_or(Duration::ZERO);
262+
}
263+
264+
if self.current_pts.is_none() {
265+
return Duration::ZERO;
266+
}
267+
268+
let frame_ptr = self.decoder.current_frame();
269+
let frame = AudioFrame::new(frame_ptr, self.time_base)
270+
.with_timeline_origin(self.timeline_origin_pts);
271+
let sample_rate = frame.frame_sample_rate();
272+
273+
frame.pts().map_or(Duration::ZERO, |pts| {
274+
if sample_rate > 0 {
275+
let frame_duration_us =
276+
(frame.samples() as u64).saturating_mul(1_000_000) / sample_rate as u64;
277+
pts.saturating_add(Duration::from_micros(frame_duration_us))
278+
} else {
279+
pts
280+
}
281+
})
282+
}
283+
237284
/// Scans the audio stream to determine its exact total duration.
238285
///
239286
/// This operation performs internal seeking and state resets. It is recommended to
@@ -248,15 +295,10 @@ impl DecodeEngine {
248295
/// * `Ok(None)` if the file is completely empty or lacks valid timestamp data.
249296
/// * `Err(AudioError)` if an I/O or parsing failure halts the scanning process.
250297
pub fn scan_duration(&mut self, mode: ScanMode) -> Result<Option<Duration>> {
251-
let original_position = if self.has_buffered_seek_frame {
252-
let frame_ptr = self.decoder.current_frame();
253-
AudioFrame::new(frame_ptr, self.time_base)
254-
.with_offset(self.buffered_seek_offset)
255-
.pts()
256-
} else {
257-
self.current_pts
258-
}
259-
.unwrap_or(Duration::ZERO);
298+
let was_exhausted = self.is_exhausted;
299+
let original_current_pts = self.current_pts;
300+
301+
let original_position = self.next_read_position();
260302

261303
self.seek(Duration::ZERO, SeekMode::Coarse)?;
262304

@@ -323,7 +365,15 @@ impl DecodeEngine {
323365
},
324366
}
325367

326-
let seek_result = self.seek(original_position, SeekMode::Accurate);
368+
let seek_result = if was_exhausted {
369+
self.is_exhausted = true;
370+
self.current_pts = original_current_pts;
371+
self.has_buffered_seek_frame = false;
372+
self.buffered_seek_offset = 0;
373+
Ok(())
374+
} else {
375+
self.seek(original_position, SeekMode::Accurate)
376+
};
327377

328378
if let Some(e) = scan_error {
329379
return Err(e);

crates/ffmpeg_audio/src/lib.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,15 @@ impl AudioReader {
130130
/// internal FFmpeg `SwrContext` allocation and initialization fail.
131131
pub fn build_resampler(&self, options: ResampleOptions) -> Result<Resampler> {
132132
let decoder = self.engine.decoder();
133-
Resampler::new(
134-
&decoder.channel_layout(),
135-
decoder.sample_fmt(),
136-
decoder.sample_rate(),
137-
options,
138-
)
133+
// Decoder-derived FFmpeg layouts are initialized and remain valid for this call.
134+
unsafe {
135+
Resampler::new(
136+
&decoder.channel_layout(),
137+
decoder.sample_fmt(),
138+
decoder.sample_rate(),
139+
options,
140+
)
141+
}
139142
}
140143

141144
/// Consumes the current [`AudioReader`] and wraps it in a [`ResampledReader`]

0 commit comments

Comments
 (0)