@@ -121,6 +121,71 @@ def get_duration(self) -> float:
121121
122122 raise ValueError (f"Could not determine duration for file '{ self .__file } '" )
123123
124+ def get_frame_count (self ) -> int :
125+ """
126+ Returns the number of frames in the video without materializing them as
127+ torch tensors.
128+ """
129+ if isinstance (self .__file , io .BytesIO ):
130+ self .__file .seek (0 )
131+
132+ with av .open (self .__file , mode = "r" ) as container :
133+ video_stream = self ._get_first_video_stream (container )
134+ # 1. Prefer the frames field if available
135+ if video_stream .frames and video_stream .frames > 0 :
136+ return int (video_stream .frames )
137+
138+ # 2. Try to estimate from duration and average_rate using only metadata
139+ if container .duration is not None and video_stream .average_rate :
140+ duration_seconds = float (container .duration / av .time_base )
141+ estimated_frames = int (round (duration_seconds * float (video_stream .average_rate )))
142+ if estimated_frames > 0 :
143+ return estimated_frames
144+
145+ if (
146+ getattr (video_stream , "duration" , None ) is not None
147+ and getattr (video_stream , "time_base" , None ) is not None
148+ and video_stream .average_rate
149+ ):
150+ duration_seconds = float (video_stream .duration * video_stream .time_base )
151+ estimated_frames = int (round (duration_seconds * float (video_stream .average_rate )))
152+ if estimated_frames > 0 :
153+ return estimated_frames
154+
155+ # 3. Last resort: decode frames and count them (streaming)
156+ frame_count = 0
157+ container .seek (0 )
158+ for packet in container .demux (video_stream ):
159+ for _ in packet .decode ():
160+ frame_count += 1
161+
162+ if frame_count == 0 :
163+ raise ValueError (f"Could not determine frame count for file '{ self .__file } '" )
164+ return frame_count
165+
166+ def get_frame_rate (self ) -> Fraction :
167+ """
168+ Returns the average frame rate of the video using container metadata
169+ without decoding all frames.
170+ """
171+ if isinstance (self .__file , io .BytesIO ):
172+ self .__file .seek (0 )
173+
174+ with av .open (self .__file , mode = "r" ) as container :
175+ video_stream = self ._get_first_video_stream (container )
176+ # Preferred: use PyAV's average_rate (usually already a Fraction-like)
177+ if video_stream .average_rate :
178+ return Fraction (video_stream .average_rate )
179+
180+ # Fallback: estimate from frames + duration if available
181+ if video_stream .frames and container .duration :
182+ duration_seconds = float (container .duration / av .time_base )
183+ if duration_seconds > 0 :
184+ return Fraction (video_stream .frames / duration_seconds ).limit_denominator ()
185+
186+ # Last resort: match get_components_internal default
187+ return Fraction (1 )
188+
124189 def get_container_format (self ) -> str :
125190 """
126191 Returns the container format of the video (e.g., 'mp4', 'mov', 'avi').
@@ -238,6 +303,13 @@ def save_to(
238303 packet .stream = stream_map [packet .stream ]
239304 output_container .mux (packet )
240305
306+ def _get_first_video_stream (self , container : InputContainer ):
307+ video_stream = next ((s for s in container .streams if s .type == "video" ), None )
308+ if video_stream is None :
309+ raise ValueError (f"No video stream found in file '{ self .__file } '" )
310+ return video_stream
311+
312+
241313class VideoFromComponents (VideoInput ):
242314 """
243315 Class representing video input from tensors.
0 commit comments