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    /// Construct a `CMTime` from a floating-point number of seconds
286    /// with the requested `preferred_timescale` (typically `600` for
287    /// video, `48000` / `44100` for audio). Wraps `CMTimeMakeWithSeconds`.
288    #[must_use]
289    pub fn from_seconds(seconds: f64, preferred_timescale: i32) -> Self {
290        extern "C" {
291            fn CMTimeMakeWithSeconds(seconds: f64, preferredTimescale: i32) -> CMTime;
292        }
293        unsafe { CMTimeMakeWithSeconds(seconds, preferred_timescale) }
294    }
295
296    /// Add two times. Wraps `CMTimeAdd`. Returns
297    /// [`CMTime::INVALID`] if either operand is invalid.
298    #[must_use]
299    #[allow(clippy::should_implement_trait)]
300    pub fn add(self, other: Self) -> Self {
301        extern "C" {
302            fn CMTimeAdd(addend1: CMTime, addend2: CMTime) -> CMTime;
303        }
304        unsafe { CMTimeAdd(self, other) }
305    }
306
307    /// Subtract `other` from `self`. Wraps `CMTimeSubtract`.
308    #[must_use]
309    #[allow(clippy::should_implement_trait)]
310    pub fn subtract(self, other: Self) -> Self {
311        extern "C" {
312            fn CMTimeSubtract(minuend: CMTime, subtrahend: CMTime) -> CMTime;
313        }
314        unsafe { CMTimeSubtract(self, other) }
315    }
316
317    /// Multiply by an integer. Wraps `CMTimeMultiply`.
318    #[must_use]
319    pub fn multiply(self, multiplier: i32) -> Self {
320        extern "C" {
321            fn CMTimeMultiply(time: CMTime, multiplier: i32) -> CMTime;
322        }
323        unsafe { CMTimeMultiply(self, multiplier) }
324    }
325
326    /// Multiply by an `f64` factor. Wraps `CMTimeMultiplyByFloat64`.
327    #[must_use]
328    pub fn multiply_by_f64(self, factor: f64) -> Self {
329        extern "C" {
330            fn CMTimeMultiplyByFloat64(time: CMTime, multiplier: f64) -> CMTime;
331        }
332        unsafe { CMTimeMultiplyByFloat64(self, factor) }
333    }
334
335    /// Compare two times. Returns `Ordering::Less` if `self < other`,
336    /// `Greater` if `self > other`, `Equal` otherwise. Wraps
337    /// `CMTimeCompare`.
338    #[must_use]
339    pub fn compare(self, other: Self) -> core::cmp::Ordering {
340        extern "C" {
341            fn CMTimeCompare(time1: CMTime, time2: CMTime) -> i32;
342        }
343        let c = unsafe { CMTimeCompare(self, other) };
344        c.cmp(&0)
345    }
346
347    /// Convert this time to a different `new_timescale`, applying
348    /// Apple's default rounding (`kCMTimeRoundingMethod_Default`).
349    /// Wraps `CMTimeConvertScale`.
350    #[must_use]
351    pub fn convert_scale(self, new_timescale: i32) -> Self {
352        extern "C" {
353            fn CMTimeConvertScale(time: CMTime, newTimescale: i32, method: u32) -> CMTime;
354        }
355        unsafe { CMTimeConvertScale(self, new_timescale, 0) }
356    }
357}
358
359impl Default for CMTime {
360    fn default() -> Self {
361        Self::INVALID
362    }
363}
364
365impl fmt::Display for CMTime {
366    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
367        if let Some(seconds) = self.as_seconds() {
368            write!(f, "{seconds:.3}s")
369        } else {
370            write!(f, "invalid")
371        }
372    }
373}
374
375/// `CMClock` wrapper for synchronization clock
376///
377/// Represents a Core Media clock used for time synchronization.
378/// Available on macOS 13.0+.
379pub struct CMClock {
380    ptr: *const c_void,
381}
382
383impl PartialEq for CMClock {
384    fn eq(&self, other: &Self) -> bool {
385        self.ptr == other.ptr
386    }
387}
388
389impl Eq for CMClock {}
390
391impl std::hash::Hash for CMClock {
392    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
393        self.ptr.hash(state);
394    }
395}
396
397impl CMClock {
398    /// Create from raw pointer, returning None if null
399    #[must_use] 
400    pub fn from_raw(ptr: *const c_void) -> Option<Self> {
401        if ptr.is_null() {
402            None
403        } else {
404            Some(Self { ptr })
405        }
406    }
407
408    /// Create from raw pointer (used internally)
409    ///
410    /// # Safety
411    /// The caller must ensure the pointer is a valid, retained `CMClock` pointer.
412    #[allow(dead_code)]
413    pub(crate) const fn from_ptr(ptr: *const c_void) -> Self {
414        Self { ptr }
415    }
416
417    /// Returns the raw pointer to the underlying `CMClock`
418    #[must_use] 
419    pub const fn as_ptr(&self) -> *const c_void {
420        self.ptr
421    }
422
423    /// Get the current time from this clock
424    ///
425    /// Note: Returns invalid time. Use `as_ptr()` with Core Media APIs directly
426    /// for full clock functionality.
427    #[must_use] 
428    pub const fn time(&self) -> CMTime {
429        // This would require FFI to CMClockGetTime - for now return invalid
430        // Users can use the pointer directly with Core Media APIs
431        CMTime::INVALID
432    }
433}
434
435impl Drop for CMClock {
436    fn drop(&mut self) {
437        if !self.ptr.is_null() {
438            // CMClock is a CFType, needs CFRelease
439            extern "C" {
440                fn CFRelease(cf: *const c_void);
441            }
442            unsafe {
443                CFRelease(self.ptr);
444            }
445        }
446    }
447}
448
449impl Clone for CMClock {
450    fn clone(&self) -> Self {
451        if self.ptr.is_null() {
452            Self {
453                ptr: std::ptr::null(),
454            }
455        } else {
456            extern "C" {
457                fn CFRetain(cf: *const c_void) -> *const c_void;
458            }
459            unsafe {
460                Self {
461                    ptr: CFRetain(self.ptr),
462                }
463            }
464        }
465    }
466}
467
468impl std::fmt::Debug for CMClock {
469    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
470        f.debug_struct("CMClock").field("ptr", &self.ptr).finish()
471    }
472}
473
474impl fmt::Display for CMClock {
475    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
476        if self.ptr.is_null() {
477            write!(f, "CMClock(null)")
478        } else {
479            write!(f, "CMClock({:p})", self.ptr)
480        }
481    }
482}
483
484// Safety: CMClock is a CoreFoundation type that is thread-safe
485unsafe impl Send for CMClock {}
486unsafe impl Sync for CMClock {}