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    const FLAG_VALID: u32 = 1 << 0;
177    const FLAG_HAS_BEEN_ROUNDED: u32 = 1 << 1;
178    const FLAG_POSITIVE_INFINITY: u32 = 1 << 2;
179    const FLAG_NEGATIVE_INFINITY: u32 = 1 << 3;
180    const FLAG_INDEFINITE: u32 = 1 << 4;
181    const FLAG_IMPLIED_VALUE_MASK: u32 =
182        Self::FLAG_POSITIVE_INFINITY | Self::FLAG_NEGATIVE_INFINITY | Self::FLAG_INDEFINITE;
183
184    /// Core Media's zero time value (`kCMTimeZero`).
185    pub const ZERO: Self = Self {
186        value: 0,
187        timescale: 1,
188        flags: Self::FLAG_VALID,
189        epoch: 0,
190    };
191
192    /// Core Media's invalid time sentinel (`kCMTimeInvalid`).
193    pub const INVALID: Self = Self {
194        value: 0,
195        timescale: 0,
196        flags: 0,
197        epoch: 0,
198    };
199
200    /// Creates a numeric `CMTime`, or [`Self::INVALID`] for a nonpositive timescale.
201    #[must_use]
202    pub const fn new(value: i64, timescale: i32) -> Self {
203        if timescale > 0 {
204            Self {
205                value,
206                timescale,
207                flags: Self::FLAG_VALID,
208                epoch: 0,
209            }
210        } else {
211            Self::INVALID
212        }
213    }
214
215    /// Returns whether this time carries Core Media's valid flag.
216    #[must_use]
217    pub const fn is_valid(&self) -> bool {
218        self.flags & Self::FLAG_VALID != 0
219    }
220
221    /// Check if this time is finite and numeric.
222    #[must_use]
223    pub const fn is_numeric(&self) -> bool {
224        self.flags & (Self::FLAG_VALID | Self::FLAG_IMPLIED_VALUE_MASK) == Self::FLAG_VALID
225    }
226
227    /// Check if this time represents zero
228    #[must_use]
229    pub fn is_zero(&self) -> bool {
230        self.compare(Self::ZERO).is_eq()
231    }
232
233    /// Check if this time is indefinite
234    #[must_use]
235    pub const fn is_indefinite(&self) -> bool {
236        self.is_valid() && self.flags & Self::FLAG_INDEFINITE != 0
237    }
238
239    /// Check if this time is positive infinity
240    #[must_use]
241    pub const fn is_positive_infinity(&self) -> bool {
242        self.is_valid() && self.flags & Self::FLAG_POSITIVE_INFINITY != 0
243    }
244
245    /// Check if this time is negative infinity
246    #[must_use]
247    pub const fn is_negative_infinity(&self) -> bool {
248        self.is_valid() && self.flags & Self::FLAG_NEGATIVE_INFINITY != 0
249    }
250
251    /// Check if this time has been rounded
252    #[must_use]
253    pub const fn has_been_rounded(&self) -> bool {
254        self.is_numeric() && self.flags & Self::FLAG_HAS_BEEN_ROUNDED != 0
255    }
256
257    /// Compare two times using Core Media's time semantics.
258    #[must_use]
259    pub fn equals(&self, other: &Self) -> bool {
260        self.compare(*other).is_eq()
261    }
262
263    /// Create a time representing positive infinity
264    #[must_use]
265    pub const fn positive_infinity() -> Self {
266        Self {
267            value: 0,
268            timescale: 0,
269            flags: Self::FLAG_VALID | Self::FLAG_POSITIVE_INFINITY,
270            epoch: 0,
271        }
272    }
273
274    /// Create a time representing negative infinity
275    #[must_use]
276    pub const fn negative_infinity() -> Self {
277        Self {
278            value: 0,
279            timescale: 0,
280            flags: Self::FLAG_VALID | Self::FLAG_NEGATIVE_INFINITY,
281            epoch: 0,
282        }
283    }
284
285    /// Create an indefinite time
286    #[must_use]
287    pub const fn indefinite() -> Self {
288        Self {
289            value: 0,
290            timescale: 0,
291            flags: Self::FLAG_VALID | Self::FLAG_INDEFINITE,
292            epoch: 0,
293        }
294    }
295
296    /// Converts a finite numeric time with a positive timescale to seconds.
297    #[must_use]
298    pub fn as_seconds(&self) -> Option<f64> {
299        if self.is_numeric() && self.timescale > 0 {
300            // Precision loss is acceptable for time conversion to seconds
301            #[allow(clippy::cast_precision_loss)]
302            Some(self.value as f64 / f64::from(self.timescale))
303        } else {
304            None
305        }
306    }
307
308    /// Construct a `CMTime` from a floating-point number of seconds
309    /// with the requested `preferred_timescale` (typically `600` for
310    /// video, `48000` / `44100` for audio). Wraps `CMTimeMakeWithSeconds`.
311    #[must_use]
312    pub fn from_seconds(seconds: f64, preferred_timescale: i32) -> Self {
313        extern "C" {
314            fn CMTimeMakeWithSeconds(seconds: f64, preferredTimescale: i32) -> CMTime;
315        }
316        unsafe { CMTimeMakeWithSeconds(seconds, preferred_timescale) }
317    }
318
319    /// Add two times. Wraps `CMTimeAdd`. Returns
320    /// [`CMTime::INVALID`] if either operand is invalid.
321    #[must_use]
322    #[allow(clippy::should_implement_trait)]
323    pub fn add(self, other: Self) -> Self {
324        extern "C" {
325            fn CMTimeAdd(addend1: CMTime, addend2: CMTime) -> CMTime;
326        }
327        unsafe { CMTimeAdd(self, other) }
328    }
329
330    /// Subtract `other` from `self`. Wraps `CMTimeSubtract`.
331    #[must_use]
332    #[allow(clippy::should_implement_trait)]
333    pub fn subtract(self, other: Self) -> Self {
334        extern "C" {
335            fn CMTimeSubtract(minuend: CMTime, subtrahend: CMTime) -> CMTime;
336        }
337        unsafe { CMTimeSubtract(self, other) }
338    }
339
340    /// Multiply by an integer. Wraps `CMTimeMultiply`.
341    #[must_use]
342    pub fn multiply(self, multiplier: i32) -> Self {
343        extern "C" {
344            fn CMTimeMultiply(time: CMTime, multiplier: i32) -> CMTime;
345        }
346        unsafe { CMTimeMultiply(self, multiplier) }
347    }
348
349    /// Multiply by an `f64` factor. Wraps `CMTimeMultiplyByFloat64`.
350    #[must_use]
351    pub fn multiply_by_f64(self, factor: f64) -> Self {
352        extern "C" {
353            fn CMTimeMultiplyByFloat64(time: CMTime, multiplier: f64) -> CMTime;
354        }
355        unsafe { CMTimeMultiplyByFloat64(self, factor) }
356    }
357
358    /// Compare two times. Returns `Ordering::Less` if `self < other`,
359    /// `Greater` if `self > other`, `Equal` otherwise. Wraps
360    /// `CMTimeCompare`.
361    #[must_use]
362    pub fn compare(self, other: Self) -> core::cmp::Ordering {
363        extern "C" {
364            fn CMTimeCompare(time1: CMTime, time2: CMTime) -> i32;
365        }
366        let c = unsafe { CMTimeCompare(self, other) };
367        c.cmp(&0)
368    }
369
370    /// Convert this time to a different `new_timescale`, applying
371    /// Apple's default rounding (`kCMTimeRoundingMethod_Default`).
372    /// Wraps `CMTimeConvertScale`.
373    #[must_use]
374    pub fn convert_scale(self, new_timescale: i32) -> Self {
375        extern "C" {
376            fn CMTimeConvertScale(time: CMTime, newTimescale: i32, method: u32) -> CMTime;
377        }
378        unsafe { CMTimeConvertScale(self, new_timescale, 1) }
379    }
380}
381
382impl Default for CMTime {
383    fn default() -> Self {
384        Self::INVALID
385    }
386}
387
388impl fmt::Display for CMTime {
389    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390        if self.is_indefinite() {
391            f.write_str("indefinite")
392        } else if self.is_positive_infinity() {
393            f.write_str("+infinity")
394        } else if self.is_negative_infinity() {
395            f.write_str("-infinity")
396        } else if let Some(seconds) = self.as_seconds() {
397            write!(f, "{seconds:.3}s")
398        } else {
399            write!(f, "invalid")
400        }
401    }
402}
403
404/// `CMTimeRange` representation matching Core Media's `CMTimeRange`.
405///
406/// ```
407/// use apple_cf::cm::{CMTime, CMTimeRange};
408///
409/// let range = CMTimeRange::new(CMTime::new(0, 600), CMTime::new(300, 600));
410/// assert_eq!(range.end(), CMTime::new(300, 600));
411/// assert!(range.contains_time(CMTime::new(150, 600)));
412/// ```
413#[repr(C)]
414#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
415pub struct CMTimeRange {
416    pub start: CMTime,
417    pub duration: CMTime,
418}
419
420impl CMTimeRange {
421    /// Core Media's invalid time-range sentinel (`kCMTimeRangeInvalid`).
422    pub const INVALID: Self = Self {
423        start: CMTime::INVALID,
424        duration: CMTime::INVALID,
425    };
426
427    /// Creates a Core Media time range from a start time and duration.
428    #[must_use]
429    pub const fn new(start: CMTime, duration: CMTime) -> Self {
430        Self { start, duration }
431    }
432
433    /// Returns the range end time via `CMTimeRangeGetEnd`.
434    #[must_use]
435    pub fn end(&self) -> CMTime {
436        extern "C" {
437            fn CMTimeRangeGetEnd(range: CMTimeRange) -> CMTime;
438        }
439        unsafe { CMTimeRangeGetEnd(*self) }
440    }
441
442    /// Returns whether both the start and duration are valid `CMTime` values.
443    #[must_use]
444    pub const fn is_valid(&self) -> bool {
445        self.start.is_valid() && self.duration.is_valid()
446    }
447
448    /// Returns whether this range contains the supplied `CMTime`.
449    #[must_use]
450    pub fn contains_time(&self, time: CMTime) -> bool {
451        extern "C" {
452            fn CMTimeRangeContainsTime(range: CMTimeRange, time: CMTime) -> bool;
453        }
454        unsafe { CMTimeRangeContainsTime(*self, time) }
455    }
456
457    /// Returns whether this range fully contains `other`.
458    #[must_use]
459    pub fn contains_range(&self, other: Self) -> bool {
460        extern "C" {
461            fn CMTimeRangeContainsTimeRange(range: CMTimeRange, otherRange: CMTimeRange) -> bool;
462        }
463        unsafe { CMTimeRangeContainsTimeRange(*self, other) }
464    }
465
466    /// Returns the intersection of this range and `other`.
467    #[must_use]
468    pub fn intersection(&self, other: Self) -> Self {
469        extern "C" {
470            fn CMTimeRangeGetIntersection(
471                range: CMTimeRange,
472                otherRange: CMTimeRange,
473            ) -> CMTimeRange;
474        }
475        unsafe { CMTimeRangeGetIntersection(*self, other) }
476    }
477
478    /// Returns the union of this range and `other`.
479    #[must_use]
480    pub fn union(&self, other: Self) -> Self {
481        extern "C" {
482            fn CMTimeRangeGetUnion(range: CMTimeRange, otherRange: CMTimeRange) -> CMTimeRange;
483        }
484        unsafe { CMTimeRangeGetUnion(*self, other) }
485    }
486}
487
488impl fmt::Display for CMTimeRange {
489    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
490        write!(
491            f,
492            "CMTimeRange(start: {}, duration: {})",
493            self.start, self.duration
494        )
495    }
496}
497
498/// `CMClock` wrapper for synchronization clock
499///
500/// Represents a Core Media clock used for time synchronization.
501/// Available on macOS 13.0+.
502pub struct CMClock {
503    ptr: *const c_void,
504}
505
506impl PartialEq for CMClock {
507    fn eq(&self, other: &Self) -> bool {
508        self.ptr == other.ptr
509    }
510}
511
512impl Eq for CMClock {}
513
514impl std::hash::Hash for CMClock {
515    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
516        self.ptr.hash(state);
517    }
518}
519
520impl CMClock {
521    /// Adopts a +1 retained `CMClockRef` and returns `None` for null.
522    ///
523    /// # Safety
524    ///
525    /// A non-null `ptr` must be a live `CMClockRef` of the exact type carrying
526    /// one retain transferred to this wrapper. The caller must not release or
527    /// separately adopt that transferred retain.
528    #[must_use]
529    pub unsafe fn from_raw(ptr: *const c_void) -> Option<Self> {
530        if ptr.is_null() {
531            None
532        } else {
533            Some(Self { ptr })
534        }
535    }
536
537    /// Retains a +0 borrowed `CMClockRef` and returns an owned wrapper.
538    ///
539    /// # Safety
540    ///
541    /// A non-null `ptr` must be a live `CMClockRef` of the exact type for the
542    /// duration of the retain call.
543    #[must_use]
544    pub unsafe fn from_raw_borrowed(ptr: *const c_void) -> Option<Self> {
545        if ptr.is_null() {
546            None
547        } else {
548            extern "C" {
549                fn CFRetain(cf: *const c_void) -> *const c_void;
550            }
551            let retained = unsafe { CFRetain(ptr) };
552            unsafe { Self::from_raw(retained) }
553        }
554    }
555
556    /// Host-time master clock.
557    #[must_use]
558    pub fn host_time_clock() -> Self {
559        extern "C" {
560            fn CMClockGetHostTimeClock() -> *const c_void;
561            fn CFRetain(cf: *const c_void) -> *const c_void;
562        }
563        let ptr = unsafe { CMClockGetHostTimeClock() };
564        assert!(!ptr.is_null(), "CMClockGetHostTimeClock returned NULL");
565        let retained = unsafe { CFRetain(ptr) };
566        Self { ptr: retained }
567    }
568
569    /// Wraps a raw `CMClockRef` by taking ownership without retaining it.
570    ///
571    /// # Safety
572    /// `ptr` must be a non-null, live `CMClockRef` of the exact type carrying
573    /// one retain transferred to this wrapper.
574    #[allow(dead_code)]
575    pub(crate) const unsafe fn from_ptr(ptr: *const c_void) -> Self {
576        Self { ptr }
577    }
578
579    /// Borrow the raw +0 `CMClockRef` while `self` remains alive.
580    #[must_use]
581    pub const fn as_ptr(&self) -> *const c_void {
582        self.ptr
583    }
584
585    /// Get the current time from this clock
586    ///
587    /// Note: Returns invalid time. Use `as_ptr()` with Core Media APIs directly
588    /// for full clock functionality.
589    #[must_use]
590    pub const fn time(&self) -> CMTime {
591        // This would require FFI to CMClockGetTime - for now return invalid
592        // Users can use the pointer directly with Core Media APIs
593        CMTime::INVALID
594    }
595}
596
597impl Drop for CMClock {
598    fn drop(&mut self) {
599        if !self.ptr.is_null() {
600            // CMClock is a CFType, needs CFRelease
601            extern "C" {
602                fn CFRelease(cf: *const c_void);
603            }
604            unsafe {
605                CFRelease(self.ptr);
606            }
607        }
608    }
609}
610
611impl Clone for CMClock {
612    fn clone(&self) -> Self {
613        if self.ptr.is_null() {
614            Self {
615                ptr: std::ptr::null(),
616            }
617        } else {
618            extern "C" {
619                fn CFRetain(cf: *const c_void) -> *const c_void;
620            }
621            unsafe {
622                Self {
623                    ptr: CFRetain(self.ptr),
624                }
625            }
626        }
627    }
628}
629
630impl std::fmt::Debug for CMClock {
631    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
632        f.debug_struct("CMClock").field("ptr", &self.ptr).finish()
633    }
634}
635
636impl fmt::Display for CMClock {
637    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
638        if self.ptr.is_null() {
639            write!(f, "CMClock(null)")
640        } else {
641            write!(f, "CMClock({:p})", self.ptr)
642        }
643    }
644}
645
646// SAFETY: `CMClockRef` is a Core Foundation type documented by Apple as
647// thread-safe; time queries are read-only operations on an opaque pointer.
648unsafe impl Send for CMClock {}
649unsafe impl Sync for CMClock {}