@@ -105,6 +105,37 @@ impl<'a> AudioFrame<'a> {
105105 unsafe { ( * self . ptr . as_ptr ( ) ) . sample_rate }
106106 }
107107
108+ /// Converts a time span in microseconds to the corresponding number of samples based on the
109+ /// current frame sample rate
110+ pub ( crate ) fn calc_samples ( & self , micros : i64 ) -> usize {
111+ let sample_rate = i64:: from ( self . frame_sample_rate ( ) ) ;
112+
113+ if sample_rate > 0 && micros > 0 {
114+ let samples_i64 = ( ( micros * sample_rate) + 999_999 ) / 1_000_000 ;
115+ samples_i64 as usize
116+ } else {
117+ 0
118+ }
119+ }
120+
121+ /// Returns the exact physical duration of this frame in microseconds.
122+ pub ( crate ) fn duration_micros ( & self ) -> i64 {
123+ let sample_rate = i64:: from ( self . frame_sample_rate ( ) ) ;
124+ if sample_rate > 0 {
125+ ( self . samples ( ) as i64 * 1_000_000 ) / sample_rate
126+ } else {
127+ 0
128+ }
129+ }
130+
131+ /// Returns the physical end time of this frame in microseconds.
132+ ///
133+ /// Returns None if the frame lacks a valid start PTS.
134+ pub ( crate ) fn end_micros ( & self ) -> Option < i64 > {
135+ self . pts_micros ( )
136+ . map ( |start| start. saturating_add ( self . duration_micros ( ) ) )
137+ }
138+
108139 /// Returns the PTS in microseconds relative to the stream timeline origin, if available.
109140 pub ( crate ) fn pts_micros ( & self ) -> Option < i64 > {
110141 let raw_pts = unsafe { ( * self . ptr . as_ptr ( ) ) . pts } ;
@@ -125,6 +156,12 @@ impl<'a> AudioFrame<'a> {
125156 } )
126157 }
127158
159+ /// Returns the precise playback duration of this audio frame.
160+ #[ must_use]
161+ pub fn duration ( & self ) -> Duration {
162+ Duration :: from_micros ( self . duration_micros ( ) . cast_unsigned ( ) )
163+ }
164+
128165 /// Returns the Presentation Timestamp (PTS) of this frame relative to the stream timeline
129166 /// origin, if available.
130167 ///
@@ -135,23 +172,8 @@ impl<'a> AudioFrame<'a> {
135172 /// - `None` if the underlying frame lacks a valid PTS (`AV_NOPTS_VALUE`).
136173 #[ must_use]
137174 pub fn pts ( & self ) -> Option < Duration > {
138- let raw_pts = unsafe { ( * self . ptr . as_ptr ( ) ) . pts } ;
139- if raw_pts == sys:: AV_NOPTS_VALUE {
140- return None ;
141- }
142- let relative_pts = raw_pts. saturating_sub ( self . timeline_origin_pts ) ;
143-
144- self . time_base
145- . calc_duration ( relative_pts)
146- . map ( |mut duration| {
147- let sample_rate = self . frame_sample_rate ( ) ;
148-
149- if self . sample_offset > 0 && sample_rate > 0 {
150- let offset_secs = self . sample_offset as f64 / f64:: from ( sample_rate) ;
151- duration += Duration :: from_secs_f64 ( offset_secs) ;
152- }
153- duration
154- } )
175+ self . pts_micros ( )
176+ . map ( |micros| Duration :: from_micros ( micros. max ( 0 ) . cast_unsigned ( ) ) )
155177 }
156178
157179 /// Zero-copy extraction of raw PCM audio data directly from the underlying FFmpeg AVFrame.
@@ -213,6 +235,8 @@ mod tests {
213235 time:: Duration ,
214236 } ;
215237
238+ use ffmpeg_audio_sys:: MICROSECONDS_Q ;
239+
216240 use super :: * ;
217241
218242 #[ test]
@@ -222,12 +246,7 @@ mod tests {
222246 raw_frame. nb_samples = 1_024 ;
223247 raw_frame. sample_rate = 48_000 ;
224248
225- let time_base = TimeBase :: try_new ( sys:: AVRational {
226- num : 1 ,
227- den : 1_000_000 ,
228- } )
229- . unwrap ( ) ;
230-
249+ let time_base = TimeBase :: try_new ( MICROSECONDS_Q ) . unwrap ( ) ;
231250 let frame = AudioFrame :: new ( & raw const raw_frame, time_base)
232251 . with_timeline_origin ( 10_000_000 )
233252 . with_offset ( 48 ) ;
@@ -241,4 +260,45 @@ mod tests {
241260 assert_eq ! ( no_pts_frame. pts_micros( ) , None ) ;
242261 assert_eq ! ( no_pts_frame. pts( ) , None ) ;
243262 }
263+
264+ #[ test]
265+ fn test_frame_time_boundary_calculations ( ) {
266+ let mut raw_frame = unsafe { mem:: zeroed :: < sys:: AVFrame > ( ) } ;
267+ raw_frame. pts = 10_000 ;
268+ raw_frame. nb_samples = 480 ;
269+ raw_frame. sample_rate = 48_000 ;
270+
271+ let time_base = TimeBase :: try_new ( MICROSECONDS_Q ) . unwrap ( ) ;
272+ let frame = AudioFrame :: new ( & raw const raw_frame, time_base) . with_timeline_origin ( 0 ) ;
273+
274+ assert_eq ! ( frame. duration_micros( ) , 10_000 ) ;
275+ assert_eq ! ( frame. end_micros( ) , Some ( 20_000 ) ) ;
276+ }
277+
278+ #[ test]
279+ fn test_calc_samples_for_micros_with_ceiling ( ) {
280+ let mut raw_frame = unsafe { mem:: zeroed :: < sys:: AVFrame > ( ) } ;
281+ raw_frame. sample_rate = 44_100 ;
282+ let time_base = TimeBase :: try_new ( sys:: AVRational { num : 1 , den : 1 } ) . unwrap ( ) ;
283+ let frame = AudioFrame :: new ( & raw const raw_frame, time_base) ;
284+
285+ assert_eq ! ( frame. calc_samples( 1_000_000 ) , 44_100 ) ;
286+ assert_eq ! ( frame. calc_samples( 1 ) , 1 ) ;
287+ assert_eq ! ( frame. calc_samples( 12 ) , 1 ) ;
288+ }
289+
290+ #[ test]
291+ fn test_frame_fallback_on_invalid_sample_rate ( ) {
292+ let mut raw_frame = unsafe { mem:: zeroed :: < sys:: AVFrame > ( ) } ;
293+ raw_frame. nb_samples = 1024 ;
294+ raw_frame. pts = 1000 ;
295+ raw_frame. sample_rate = 0 ;
296+
297+ let time_base = TimeBase :: try_new ( MICROSECONDS_Q ) . unwrap ( ) ;
298+ let frame = AudioFrame :: new ( & raw const raw_frame, time_base) ;
299+
300+ assert_eq ! ( frame. duration_micros( ) , 0 ) ;
301+ assert_eq ! ( frame. end_micros( ) , Some ( 1000 ) ) ;
302+ assert_eq ! ( frame. calc_samples( 5_000_000 ) , 0 ) ;
303+ }
244304}
0 commit comments