Skip to main content

apple_cf/cm/
time.rs

1//! Core Media time types
2
3use std::ffi::c_void;
4use std::fmt;
5
6/// `CMTime` representation matching Core Media's `CMTime`
7///
8/// Represents a rational time value with a 64-bit numerator and 32-bit denominator.
9///
10/// # Examples
11///
12/// ```
13/// use apple_cf::cm::CMTime;
14///
15/// // Create a time of 1 second (30/30)
16/// let time = CMTime::new(30, 30);
17/// assert_eq!(time.as_seconds(), Some(1.0));
18///
19/// // Create a time of 2.5 seconds at 1000 Hz timescale
20/// let time = CMTime::new(2500, 1000);
21/// assert_eq!(time.value, 2500);
22/// assert_eq!(time.timescale, 1000);
23/// assert_eq!(time.as_seconds(), Some(2.5));
24/// ```
25#[repr(C)]
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct CMTime {
28    pub value: i64,
29    pub timescale: i32,
30    pub flags: u32,
31    pub epoch: i64,
32}
33
34impl std::hash::Hash for CMTime {
35    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
36        self.value.hash(state);
37        self.timescale.hash(state);
38        self.flags.hash(state);
39        self.epoch.hash(state);
40    }
41}
42
43/// Sample timing information
44///
45/// Contains timing data for a media sample (audio or video frame).
46///
47/// # Examples
48///
49/// ```
50/// use apple_cf::cm::{CMSampleTimingInfo, CMTime};
51///
52/// let timing = CMSampleTimingInfo::new();
53/// assert!(!timing.is_valid());
54///
55/// let duration = CMTime::new(1, 30);
56/// let pts = CMTime::new(100, 30);
57/// let dts = CMTime::new(100, 30);
58/// let timing = CMSampleTimingInfo::with_times(duration, pts, dts);
59/// assert!(timing.is_valid());
60/// ```
61#[repr(C)]
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct CMSampleTimingInfo {
64    pub duration: CMTime,
65    pub presentation_time_stamp: CMTime,
66    pub decode_time_stamp: CMTime,
67}
68
69impl std::hash::Hash for CMSampleTimingInfo {
70    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
71        self.duration.hash(state);
72        self.presentation_time_stamp.hash(state);
73        self.decode_time_stamp.hash(state);
74    }
75}
76
77impl CMSampleTimingInfo {
78    /// Create a new timing info with all times set to invalid
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// use apple_cf::cm::CMSampleTimingInfo;
84    ///
85    /// let timing = CMSampleTimingInfo::new();
86    /// assert!(!timing.is_valid());
87    /// ```
88    #[must_use] 
89    pub const fn new() -> Self {
90        Self {
91            duration: CMTime::INVALID,
92            presentation_time_stamp: CMTime::INVALID,
93            decode_time_stamp: CMTime::INVALID,
94        }
95    }
96
97    /// Create timing info with specific values
98    #[must_use] 
99    pub const fn with_times(
100        duration: CMTime,
101        presentation_time_stamp: CMTime,
102        decode_time_stamp: CMTime,
103    ) -> Self {
104        Self {
105            duration,
106            presentation_time_stamp,
107            decode_time_stamp,
108        }
109    }
110
111    /// Check if all timing fields are valid
112    #[must_use] 
113    pub const fn is_valid(&self) -> bool {
114        self.duration.is_valid()
115            && self.presentation_time_stamp.is_valid()
116            && self.decode_time_stamp.is_valid()
117    }
118
119    /// Check if presentation timestamp is valid
120    #[must_use] 
121    pub const fn has_valid_presentation_time(&self) -> bool {
122        self.presentation_time_stamp.is_valid()
123    }
124
125    /// Check if decode timestamp is valid
126    #[must_use] 
127    pub const fn has_valid_decode_time(&self) -> bool {
128        self.decode_time_stamp.is_valid()
129    }
130
131    /// Check if duration is valid
132    #[must_use] 
133    pub const fn has_valid_duration(&self) -> bool {
134        self.duration.is_valid()
135    }
136
137    /// Get the presentation timestamp in seconds
138    #[must_use] 
139    pub fn presentation_seconds(&self) -> Option<f64> {
140        self.presentation_time_stamp.as_seconds()
141    }
142
143    /// Get the decode timestamp in seconds
144    #[must_use] 
145    pub fn decode_seconds(&self) -> Option<f64> {
146        self.decode_time_stamp.as_seconds()
147    }
148
149    /// Get the duration in seconds
150    #[must_use] 
151    pub fn duration_seconds(&self) -> Option<f64> {
152        self.duration.as_seconds()
153    }
154}
155
156impl Default for CMSampleTimingInfo {
157    fn default() -> Self {
158        Self::new()
159    }
160}
161
162impl fmt::Display for CMSampleTimingInfo {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        write!(
165            f,
166            "CMSampleTimingInfo(pts: {}, dts: {}, duration: {})",
167            self.presentation_time_stamp, self.decode_time_stamp, self.duration
168        )
169    }
170}
171
172impl CMTime {
173    pub const ZERO: Self = Self {
174        value: 0,
175        timescale: 0,
176        flags: 1,
177        epoch: 0,
178    };
179
180    pub const INVALID: Self = Self {
181        value: 0,
182        timescale: 0,
183        flags: 0,
184        epoch: 0,
185    };
186
187    #[must_use] 
188    pub const fn new(value: i64, timescale: i32) -> Self {
189        Self {
190            value,
191            timescale,
192            flags: 1,
193            epoch: 0,
194        }
195    }
196
197    #[must_use] 
198    pub const fn is_valid(&self) -> bool {
199        self.flags & 0x1 != 0
200    }
201
202    /// Check if this time represents zero
203    #[must_use] 
204    pub const fn is_zero(&self) -> bool {
205        self.value == 0 && self.is_valid()
206    }
207
208    /// Check if this time is indefinite
209    #[must_use] 
210    pub const fn is_indefinite(&self) -> bool {
211        self.flags & 0x2 != 0
212    }
213
214    /// Check if this time is positive infinity
215    #[must_use] 
216    pub const fn is_positive_infinity(&self) -> bool {
217        self.flags & 0x4 != 0
218    }
219
220    /// Check if this time is negative infinity
221    #[must_use] 
222    pub const fn is_negative_infinity(&self) -> bool {
223        self.flags & 0x8 != 0
224    }
225
226    /// Check if this time has been rounded
227    #[must_use] 
228    pub const fn has_been_rounded(&self) -> bool {
229        self.flags & 0x10 != 0
230    }
231
232    /// Compare two times for equality (value and timescale)
233    #[must_use] 
234    pub const fn equals(&self, other: &Self) -> bool {
235        if !self.is_valid() || !other.is_valid() {
236            return false;
237        }
238        self.value == other.value && self.timescale == other.timescale
239    }
240
241    /// Create a time representing positive infinity
242    #[must_use] 
243    pub const fn positive_infinity() -> Self {
244        Self {
245            value: 0,
246            timescale: 0,
247            flags: 0x5, // kCMTimeFlags_Valid | kCMTimeFlags_PositiveInfinity
248            epoch: 0,
249        }
250    }
251
252    /// Create a time representing negative infinity
253    #[must_use] 
254    pub const fn negative_infinity() -> Self {
255        Self {
256            value: 0,
257            timescale: 0,
258            flags: 0x9, // kCMTimeFlags_Valid | kCMTimeFlags_NegativeInfinity
259            epoch: 0,
260        }
261    }
262
263    /// Create an indefinite time
264    #[must_use] 
265    pub const fn indefinite() -> Self {
266        Self {
267            value: 0,
268            timescale: 0,
269            flags: 0x3, // kCMTimeFlags_Valid | kCMTimeFlags_Indefinite
270            epoch: 0,
271        }
272    }
273
274    #[must_use] 
275    pub fn as_seconds(&self) -> Option<f64> {
276        if self.is_valid() && self.timescale != 0 {
277            // Precision loss is acceptable for time conversion to seconds
278            #[allow(clippy::cast_precision_loss)]
279            Some(self.value as f64 / f64::from(self.timescale))
280        } else {
281            None
282        }
283    }
284}
285
286impl Default for CMTime {
287    fn default() -> Self {
288        Self::INVALID
289    }
290}
291
292impl fmt::Display for CMTime {
293    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294        if let Some(seconds) = self.as_seconds() {
295            write!(f, "{seconds:.3}s")
296        } else {
297            write!(f, "invalid")
298        }
299    }
300}
301
302/// `CMClock` wrapper for synchronization clock
303///
304/// Represents a Core Media clock used for time synchronization.
305/// Available on macOS 13.0+.
306pub struct CMClock {
307    ptr: *const c_void,
308}
309
310impl PartialEq for CMClock {
311    fn eq(&self, other: &Self) -> bool {
312        self.ptr == other.ptr
313    }
314}
315
316impl Eq for CMClock {}
317
318impl std::hash::Hash for CMClock {
319    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
320        self.ptr.hash(state);
321    }
322}
323
324impl CMClock {
325    /// Create from raw pointer, returning None if null
326    #[must_use] 
327    pub fn from_raw(ptr: *const c_void) -> Option<Self> {
328        if ptr.is_null() {
329            None
330        } else {
331            Some(Self { ptr })
332        }
333    }
334
335    /// Create from raw pointer (used internally)
336    ///
337    /// # Safety
338    /// The caller must ensure the pointer is a valid, retained `CMClock` pointer.
339    #[allow(dead_code)]
340    pub(crate) const fn from_ptr(ptr: *const c_void) -> Self {
341        Self { ptr }
342    }
343
344    /// Returns the raw pointer to the underlying `CMClock`
345    #[must_use] 
346    pub const fn as_ptr(&self) -> *const c_void {
347        self.ptr
348    }
349
350    /// Get the current time from this clock
351    ///
352    /// Note: Returns invalid time. Use `as_ptr()` with Core Media APIs directly
353    /// for full clock functionality.
354    #[must_use] 
355    pub const fn time(&self) -> CMTime {
356        // This would require FFI to CMClockGetTime - for now return invalid
357        // Users can use the pointer directly with Core Media APIs
358        CMTime::INVALID
359    }
360}
361
362impl Drop for CMClock {
363    fn drop(&mut self) {
364        if !self.ptr.is_null() {
365            // CMClock is a CFType, needs CFRelease
366            extern "C" {
367                fn CFRelease(cf: *const c_void);
368            }
369            unsafe {
370                CFRelease(self.ptr);
371            }
372        }
373    }
374}
375
376impl Clone for CMClock {
377    fn clone(&self) -> Self {
378        if self.ptr.is_null() {
379            Self {
380                ptr: std::ptr::null(),
381            }
382        } else {
383            extern "C" {
384                fn CFRetain(cf: *const c_void) -> *const c_void;
385            }
386            unsafe {
387                Self {
388                    ptr: CFRetain(self.ptr),
389                }
390            }
391        }
392    }
393}
394
395impl std::fmt::Debug for CMClock {
396    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
397        f.debug_struct("CMClock").field("ptr", &self.ptr).finish()
398    }
399}
400
401impl fmt::Display for CMClock {
402    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403        if self.ptr.is_null() {
404            write!(f, "CMClock(null)")
405        } else {
406            write!(f, "CMClock({:p})", self.ptr)
407        }
408    }
409}
410
411// Safety: CMClock is a CoreFoundation type that is thread-safe
412unsafe impl Send for CMClock {}
413unsafe impl Sync for CMClock {}