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