Skip to main content

apple_cf/cm/
time.rs

1#![allow(clippy::missing_panics_doc)]
2
3//! Core Media time types
4
5use std::ffi::c_void;
6use std::fmt;
7
8/// `CMTime` representation matching Core Media's `CMTime`
9///
10/// Represents a rational time value with a 64-bit numerator and 32-bit denominator.
11///
12/// # Examples
13///
14/// ```
15/// use apple_cf::cm::CMTime;
16///
17/// // Create a time of 1 second (30/30)
18/// let time = CMTime::new(30, 30);
19/// assert_eq!(time.as_seconds(), Some(1.0));
20///
21/// // Create a time of 2.5 seconds at 1000 Hz timescale
22/// let time = CMTime::new(2500, 1000);
23/// assert_eq!(time.value, 2500);
24/// assert_eq!(time.timescale, 1000);
25/// assert_eq!(time.as_seconds(), Some(2.5));
26/// ```
27#[repr(C)]
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct CMTime {
30    pub value: i64,
31    pub timescale: i32,
32    pub flags: u32,
33    pub epoch: i64,
34}
35
36impl std::hash::Hash for CMTime {
37    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
38        self.value.hash(state);
39        self.timescale.hash(state);
40        self.flags.hash(state);
41        self.epoch.hash(state);
42    }
43}
44
45/// Sample timing information
46///
47/// Contains timing data for a media sample (audio or video frame).
48///
49/// # Examples
50///
51/// ```
52/// use apple_cf::cm::{CMSampleTimingInfo, CMTime};
53///
54/// let timing = CMSampleTimingInfo::new();
55/// assert!(!timing.is_valid());
56///
57/// let duration = CMTime::new(1, 30);
58/// let pts = CMTime::new(100, 30);
59/// let dts = CMTime::new(100, 30);
60/// let timing = CMSampleTimingInfo::with_times(duration, pts, dts);
61/// assert!(timing.is_valid());
62/// ```
63#[repr(C)]
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct CMSampleTimingInfo {
66    pub duration: CMTime,
67    pub presentation_time_stamp: CMTime,
68    pub decode_time_stamp: CMTime,
69}
70
71impl std::hash::Hash for CMSampleTimingInfo {
72    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
73        self.duration.hash(state);
74        self.presentation_time_stamp.hash(state);
75        self.decode_time_stamp.hash(state);
76    }
77}
78
79impl CMSampleTimingInfo {
80    /// Create a new timing info with all times set to invalid
81    ///
82    /// # Examples
83    ///
84    /// ```
85    /// use apple_cf::cm::CMSampleTimingInfo;
86    ///
87    /// let timing = CMSampleTimingInfo::new();
88    /// assert!(!timing.is_valid());
89    /// ```
90    #[must_use]
91    pub const fn new() -> Self {
92        Self {
93            duration: CMTime::INVALID,
94            presentation_time_stamp: CMTime::INVALID,
95            decode_time_stamp: CMTime::INVALID,
96        }
97    }
98
99    /// Create timing info with specific values
100    #[must_use]
101    pub const fn with_times(
102        duration: CMTime,
103        presentation_time_stamp: CMTime,
104        decode_time_stamp: CMTime,
105    ) -> Self {
106        Self {
107            duration,
108            presentation_time_stamp,
109            decode_time_stamp,
110        }
111    }
112
113    /// Check if all timing fields are valid
114    /// Returns whether this time carries Core Media's valid flag.
115    #[must_use]
116    pub const fn is_valid(&self) -> bool {
117        self.duration.is_valid()
118            && self.presentation_time_stamp.is_valid()
119            && self.decode_time_stamp.is_valid()
120    }
121
122    /// Check if presentation timestamp is valid
123    #[must_use]
124    pub const fn has_valid_presentation_time(&self) -> bool {
125        self.presentation_time_stamp.is_valid()
126    }
127
128    /// Check if decode timestamp is valid
129    #[must_use]
130    pub const fn has_valid_decode_time(&self) -> bool {
131        self.decode_time_stamp.is_valid()
132    }
133
134    /// Check if duration is valid
135    #[must_use]
136    pub const fn has_valid_duration(&self) -> bool {
137        self.duration.is_valid()
138    }
139
140    /// Get the presentation timestamp in seconds
141    #[must_use]
142    pub fn presentation_seconds(&self) -> Option<f64> {
143        self.presentation_time_stamp.as_seconds()
144    }
145
146    /// Get the decode timestamp in seconds
147    #[must_use]
148    pub fn decode_seconds(&self) -> Option<f64> {
149        self.decode_time_stamp.as_seconds()
150    }
151
152    /// Get the duration in seconds
153    #[must_use]
154    pub fn duration_seconds(&self) -> Option<f64> {
155        self.duration.as_seconds()
156    }
157}
158
159impl Default for CMSampleTimingInfo {
160    fn default() -> Self {
161        Self::new()
162    }
163}
164
165impl fmt::Display for CMSampleTimingInfo {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167        write!(
168            f,
169            "CMSampleTimingInfo(pts: {}, dts: {}, duration: {})",
170            self.presentation_time_stamp, self.decode_time_stamp, self.duration
171        )
172    }
173}
174
175impl CMTime {
176    /// Core Media's zero time value (`kCMTimeZero`).
177    pub const ZERO: Self = Self {
178        value: 0,
179        timescale: 0,
180        flags: 1,
181        epoch: 0,
182    };
183
184    /// Core Media's invalid time sentinel (`kCMTimeInvalid`).
185    pub const INVALID: Self = Self {
186        value: 0,
187        timescale: 0,
188        flags: 0,
189        epoch: 0,
190    };
191
192    /// Creates a valid `CMTime` with the supplied value and timescale.
193    #[must_use]
194    pub const fn new(value: i64, timescale: i32) -> Self {
195        Self {
196            value,
197            timescale,
198            flags: 1,
199            epoch: 0,
200        }
201    }
202
203    /// Returns whether this time carries Core Media's valid flag.
204    #[must_use]
205    pub const fn is_valid(&self) -> bool {
206        self.flags & 0x1 != 0
207    }
208
209    /// Check if this time represents zero
210    #[must_use]
211    pub const fn is_zero(&self) -> bool {
212        self.value == 0 && self.is_valid()
213    }
214
215    /// Check if this time is indefinite
216    #[must_use]
217    pub const fn is_indefinite(&self) -> bool {
218        self.flags & 0x2 != 0
219    }
220
221    /// Check if this time is positive infinity
222    #[must_use]
223    pub const fn is_positive_infinity(&self) -> bool {
224        self.flags & 0x4 != 0
225    }
226
227    /// Check if this time is negative infinity
228    #[must_use]
229    pub const fn is_negative_infinity(&self) -> bool {
230        self.flags & 0x8 != 0
231    }
232
233    /// Check if this time has been rounded
234    #[must_use]
235    pub const fn has_been_rounded(&self) -> bool {
236        self.flags & 0x10 != 0
237    }
238
239    /// Compare two times for equality (value and timescale)
240    #[must_use]
241    pub const fn equals(&self, other: &Self) -> bool {
242        if !self.is_valid() || !other.is_valid() {
243            return false;
244        }
245        self.value == other.value && self.timescale == other.timescale
246    }
247
248    /// Create a time representing positive infinity
249    #[must_use]
250    pub const fn positive_infinity() -> Self {
251        Self {
252            value: 0,
253            timescale: 0,
254            flags: 0x5, // kCMTimeFlags_Valid | kCMTimeFlags_PositiveInfinity
255            epoch: 0,
256        }
257    }
258
259    /// Create a time representing negative infinity
260    #[must_use]
261    pub const fn negative_infinity() -> Self {
262        Self {
263            value: 0,
264            timescale: 0,
265            flags: 0x9, // kCMTimeFlags_Valid | kCMTimeFlags_NegativeInfinity
266            epoch: 0,
267        }
268    }
269
270    /// Create an indefinite time
271    #[must_use]
272    pub const fn indefinite() -> Self {
273        Self {
274            value: 0,
275            timescale: 0,
276            flags: 0x3, // kCMTimeFlags_Valid | kCMTimeFlags_Indefinite
277            epoch: 0,
278        }
279    }
280
281    /// Converts this time to seconds when it is valid and has a non-zero timescale.
282    #[must_use]
283    pub fn as_seconds(&self) -> Option<f64> {
284        if self.is_valid() && self.timescale != 0 {
285            // Precision loss is acceptable for time conversion to seconds
286            #[allow(clippy::cast_precision_loss)]
287            Some(self.value as f64 / f64::from(self.timescale))
288        } else {
289            None
290        }
291    }
292
293    /// Construct a `CMTime` from a floating-point number of seconds
294    /// with the requested `preferred_timescale` (typically `600` for
295    /// video, `48000` / `44100` for audio). Wraps `CMTimeMakeWithSeconds`.
296    #[must_use]
297    pub fn from_seconds(seconds: f64, preferred_timescale: i32) -> Self {
298        extern "C" {
299            fn CMTimeMakeWithSeconds(seconds: f64, preferredTimescale: i32) -> CMTime;
300        }
301        unsafe { CMTimeMakeWithSeconds(seconds, preferred_timescale) }
302    }
303
304    /// Add two times. Wraps `CMTimeAdd`. Returns
305    /// [`CMTime::INVALID`] if either operand is invalid.
306    #[must_use]
307    #[allow(clippy::should_implement_trait)]
308    pub fn add(self, other: Self) -> Self {
309        extern "C" {
310            fn CMTimeAdd(addend1: CMTime, addend2: CMTime) -> CMTime;
311        }
312        unsafe { CMTimeAdd(self, other) }
313    }
314
315    /// Subtract `other` from `self`. Wraps `CMTimeSubtract`.
316    #[must_use]
317    #[allow(clippy::should_implement_trait)]
318    pub fn subtract(self, other: Self) -> Self {
319        extern "C" {
320            fn CMTimeSubtract(minuend: CMTime, subtrahend: CMTime) -> CMTime;
321        }
322        unsafe { CMTimeSubtract(self, other) }
323    }
324
325    /// Multiply by an integer. Wraps `CMTimeMultiply`.
326    #[must_use]
327    pub fn multiply(self, multiplier: i32) -> Self {
328        extern "C" {
329            fn CMTimeMultiply(time: CMTime, multiplier: i32) -> CMTime;
330        }
331        unsafe { CMTimeMultiply(self, multiplier) }
332    }
333
334    /// Multiply by an `f64` factor. Wraps `CMTimeMultiplyByFloat64`.
335    #[must_use]
336    pub fn multiply_by_f64(self, factor: f64) -> Self {
337        extern "C" {
338            fn CMTimeMultiplyByFloat64(time: CMTime, multiplier: f64) -> CMTime;
339        }
340        unsafe { CMTimeMultiplyByFloat64(self, factor) }
341    }
342
343    /// Compare two times. Returns `Ordering::Less` if `self < other`,
344    /// `Greater` if `self > other`, `Equal` otherwise. Wraps
345    /// `CMTimeCompare`.
346    #[must_use]
347    pub fn compare(self, other: Self) -> core::cmp::Ordering {
348        extern "C" {
349            fn CMTimeCompare(time1: CMTime, time2: CMTime) -> i32;
350        }
351        let c = unsafe { CMTimeCompare(self, other) };
352        c.cmp(&0)
353    }
354
355    /// Convert this time to a different `new_timescale`, applying
356    /// Apple's default rounding (`kCMTimeRoundingMethod_Default`).
357    /// Wraps `CMTimeConvertScale`.
358    #[must_use]
359    pub fn convert_scale(self, new_timescale: i32) -> Self {
360        extern "C" {
361            fn CMTimeConvertScale(time: CMTime, newTimescale: i32, method: u32) -> CMTime;
362        }
363        unsafe { CMTimeConvertScale(self, new_timescale, 0) }
364    }
365}
366
367impl Default for CMTime {
368    fn default() -> Self {
369        Self::INVALID
370    }
371}
372
373impl fmt::Display for CMTime {
374    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
375        if let Some(seconds) = self.as_seconds() {
376            write!(f, "{seconds:.3}s")
377        } else {
378            write!(f, "invalid")
379        }
380    }
381}
382
383/// `CMTimeRange` representation matching Core Media's `CMTimeRange`.
384///
385/// ```
386/// use apple_cf::cm::{CMTime, CMTimeRange};
387///
388/// let range = CMTimeRange::new(CMTime::new(0, 600), CMTime::new(300, 600));
389/// assert_eq!(range.end(), CMTime::new(300, 600));
390/// assert!(range.contains_time(CMTime::new(150, 600)));
391/// ```
392#[repr(C)]
393#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
394pub struct CMTimeRange {
395    pub start: CMTime,
396    pub duration: CMTime,
397}
398
399impl CMTimeRange {
400    /// Core Media's invalid time-range sentinel (`kCMTimeRangeInvalid`).
401    pub const INVALID: Self = Self {
402        start: CMTime::INVALID,
403        duration: CMTime::INVALID,
404    };
405
406    /// Creates a Core Media time range from a start time and duration.
407    #[must_use]
408    pub const fn new(start: CMTime, duration: CMTime) -> Self {
409        Self { start, duration }
410    }
411
412    /// Returns the range end time via `CMTimeRangeGetEnd`.
413    #[must_use]
414    pub fn end(&self) -> CMTime {
415        extern "C" {
416            fn CMTimeRangeGetEnd(range: CMTimeRange) -> CMTime;
417        }
418        unsafe { CMTimeRangeGetEnd(*self) }
419    }
420
421    /// Returns whether both the start and duration are valid `CMTime` values.
422    #[must_use]
423    pub const fn is_valid(&self) -> bool {
424        self.start.is_valid() && self.duration.is_valid()
425    }
426
427    /// Returns whether this range contains the supplied `CMTime`.
428    #[must_use]
429    pub fn contains_time(&self, time: CMTime) -> bool {
430        extern "C" {
431            fn CMTimeRangeContainsTime(range: CMTimeRange, time: CMTime) -> bool;
432        }
433        unsafe { CMTimeRangeContainsTime(*self, time) }
434    }
435
436    /// Returns whether this range fully contains `other`.
437    #[must_use]
438    pub fn contains_range(&self, other: Self) -> bool {
439        extern "C" {
440            fn CMTimeRangeContainsTimeRange(range: CMTimeRange, otherRange: CMTimeRange) -> bool;
441        }
442        unsafe { CMTimeRangeContainsTimeRange(*self, other) }
443    }
444
445    /// Returns the intersection of this range and `other`.
446    #[must_use]
447    pub fn intersection(&self, other: Self) -> Self {
448        extern "C" {
449            fn CMTimeRangeGetIntersection(
450                range: CMTimeRange,
451                otherRange: CMTimeRange,
452            ) -> CMTimeRange;
453        }
454        unsafe { CMTimeRangeGetIntersection(*self, other) }
455    }
456
457    /// Returns the union of this range and `other`.
458    #[must_use]
459    pub fn union(&self, other: Self) -> Self {
460        extern "C" {
461            fn CMTimeRangeGetUnion(range: CMTimeRange, otherRange: CMTimeRange) -> CMTimeRange;
462        }
463        unsafe { CMTimeRangeGetUnion(*self, other) }
464    }
465}
466
467impl fmt::Display for CMTimeRange {
468    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
469        write!(
470            f,
471            "CMTimeRange(start: {}, duration: {})",
472            self.start, self.duration
473        )
474    }
475}
476
477/// `CMClock` wrapper for synchronization clock
478///
479/// Represents a Core Media clock used for time synchronization.
480/// Available on macOS 13.0+.
481pub struct CMClock {
482    ptr: *const c_void,
483}
484
485impl PartialEq for CMClock {
486    fn eq(&self, other: &Self) -> bool {
487        self.ptr == other.ptr
488    }
489}
490
491impl Eq for CMClock {}
492
493impl std::hash::Hash for CMClock {
494    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
495        self.ptr.hash(state);
496    }
497}
498
499impl CMClock {
500    /// Wraps a +1 retained `CMClockRef` and returns `None` for null.
501    #[must_use]
502    pub fn from_raw(ptr: *const c_void) -> Option<Self> {
503        if ptr.is_null() {
504            None
505        } else {
506            Some(Self { ptr })
507        }
508    }
509
510    /// Host-time master clock.
511    #[must_use]
512    pub fn host_time_clock() -> Self {
513        extern "C" {
514            fn CMClockGetHostTimeClock() -> *const c_void;
515            fn CFRetain(cf: *const c_void) -> *const c_void;
516        }
517        let ptr = unsafe { CMClockGetHostTimeClock() };
518        assert!(!ptr.is_null(), "CMClockGetHostTimeClock returned NULL");
519        let retained = unsafe { CFRetain(ptr) };
520        Self { ptr: retained }
521    }
522
523    /// Wraps a raw `CMClockRef` by taking ownership without retaining it.
524    ///
525    /// # Safety
526    /// The caller must ensure `ptr` is a valid +1 retained `CMClockRef`.
527    #[allow(dead_code)]
528    pub(crate) const fn from_ptr(ptr: *const c_void) -> Self {
529        Self { ptr }
530    }
531
532    /// Returns the raw pointer to the underlying `CMClock`
533    #[must_use]
534    pub const fn as_ptr(&self) -> *const c_void {
535        self.ptr
536    }
537
538    /// Get the current time from this clock
539    ///
540    /// Note: Returns invalid time. Use `as_ptr()` with Core Media APIs directly
541    /// for full clock functionality.
542    #[must_use]
543    pub const fn time(&self) -> CMTime {
544        // This would require FFI to CMClockGetTime - for now return invalid
545        // Users can use the pointer directly with Core Media APIs
546        CMTime::INVALID
547    }
548}
549
550impl Drop for CMClock {
551    fn drop(&mut self) {
552        if !self.ptr.is_null() {
553            // CMClock is a CFType, needs CFRelease
554            extern "C" {
555                fn CFRelease(cf: *const c_void);
556            }
557            unsafe {
558                CFRelease(self.ptr);
559            }
560        }
561    }
562}
563
564impl Clone for CMClock {
565    fn clone(&self) -> Self {
566        if self.ptr.is_null() {
567            Self {
568                ptr: std::ptr::null(),
569            }
570        } else {
571            extern "C" {
572                fn CFRetain(cf: *const c_void) -> *const c_void;
573            }
574            unsafe {
575                Self {
576                    ptr: CFRetain(self.ptr),
577                }
578            }
579        }
580    }
581}
582
583impl std::fmt::Debug for CMClock {
584    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
585        f.debug_struct("CMClock").field("ptr", &self.ptr).finish()
586    }
587}
588
589impl fmt::Display for CMClock {
590    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
591        if self.ptr.is_null() {
592            write!(f, "CMClock(null)")
593        } else {
594            write!(f, "CMClock({:p})", self.ptr)
595        }
596    }
597}
598
599// SAFETY: `CMClockRef` is a Core Foundation type documented by Apple as
600// thread-safe; time queries are read-only operations on an opaque pointer.
601unsafe impl Send for CMClock {}
602unsafe impl Sync for CMClock {}