rskit_media/probe/analysis.rs
1//! Extended probe analysis types — keyframes, silence, chapters.
2//!
3//! These types represent structural information about media that goes beyond basic metadata.
4//! They power chunking strategies, timeline UIs, and intelligent splitting.
5
6use std::time::Duration;
7
8use serde::{Deserialize, Serialize};
9
10use crate::time::{TimeRange, Timestamp};
11
12/// A keyframe (I-frame) position within a video stream.
13///
14/// Keyframes are the only safe points to split video without re-encoding.
15/// Used by [`ChunkStrategy`](crate::chunking::ChunkStrategy) implementations to plan efficient parallel processing boundaries.
16#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
17pub struct KeyframeInfo {
18 /// Position in the stream.
19 pub timestamp: Timestamp,
20 /// Frame number (zero-based).
21 pub frame_number: u64,
22 /// Picture type from the codec (I, IDR, etc.).
23 pub picture_type: PictureType,
24 /// Packet size in bytes (useful for bitrate analysis).
25 pub size_bytes: Option<u64>,
26}
27
28/// Video picture type from the codec.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
30#[non_exhaustive]
31pub enum PictureType {
32 /// Intra-coded frame (full frame, no references).
33 I,
34 /// IDR frame (instantaneous decoder refresh — resets reference list).
35 Idr,
36 /// Predicted frame (references past frames).
37 P,
38 /// Bi-predicted frame (references past and future frames).
39 B,
40 /// Unknown or unrecognized type.
41 Unknown,
42}
43
44impl PictureType {
45 /// Parse from ffprobe's `pict_type` field.
46 #[must_use]
47 pub fn from_ffprobe(s: &str) -> Self {
48 match s.trim() {
49 "I" => Self::I,
50 "IDR" | "idr" => Self::Idr,
51 "P" => Self::P,
52 "B" => Self::B,
53 _ => Self::Unknown,
54 }
55 }
56
57 /// Whether this picture type is a keyframe (safe split point).
58 #[must_use]
59 pub fn is_keyframe(&self) -> bool {
60 matches!(self, Self::I | Self::Idr)
61 }
62}
63
64/// A detected silence interval within an audio stream.
65///
66/// Silence intervals are optimal split points for audio-centric operations like transcription —
67/// splitting at silence avoids cutting words.
68#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
69pub struct SilenceInterval {
70 /// Start of the silence.
71 pub start: Timestamp,
72 /// End of the silence.
73 pub end: Timestamp,
74 /// Duration of the silence.
75 pub duration: Duration,
76}
77
78impl SilenceInterval {
79 /// The midpoint of this silence interval — ideal split point.
80 #[must_use]
81 pub fn midpoint(&self) -> Timestamp {
82 let mid_us = (self.start.as_micros() + self.end.as_micros()) / 2;
83 Timestamp::from_micros(mid_us)
84 }
85
86 /// Convert to a [`TimeRange`].
87 #[must_use]
88 pub fn as_range(&self) -> TimeRange {
89 TimeRange::new(self.start, self.end)
90 }
91}
92
93/// A chapter marker embedded in the media container.
94///
95/// Chapters provide semantic structure (intro, verse, chorus, etc.) and are common in podcasts,
96/// audiobooks, and long-form video.
97#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
98pub struct Chapter {
99 /// Chapter index (zero-based).
100 pub index: usize,
101 /// Time range of this chapter.
102 pub range: TimeRange,
103 /// Chapter title (from container metadata).
104 pub title: Option<String>,
105}
106
107impl Chapter {
108 /// Duration of this chapter.
109 #[must_use]
110 pub fn duration(&self) -> Duration {
111 self.range.duration()
112 }
113}