Skip to content

Commit d81d1c5

Browse files
committed
fix diferent fps video
1 parent bb9e5bf commit d81d1c5

5 files changed

Lines changed: 140 additions & 24 deletions

File tree

backend/src/decoder.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,28 @@ async fn complete_pending_with_fallback(inner: Arc<Inner>) {
622622
continue;
623623
}
624624

625+
let previous_frame = {
626+
let frames = inner.frames.read().unwrap();
627+
let mut probe = frame_index;
628+
let mut found: Option<(u32, Arc<Vec<u8>>)> = None;
629+
while let Some(prev) = probe.checked_sub(1) {
630+
probe = prev;
631+
let Some(prev_future) = frames.get(&probe) else {
632+
continue;
633+
};
634+
if let Some(prev_frame) = prev_future.get_now() {
635+
found = Some((probe, prev_frame));
636+
break;
637+
}
638+
}
639+
found
640+
};
641+
if let Some((_, previous_frame)) = previous_frame {
642+
ENTIRE_CACHE_SIZE.fetch_add(previous_frame.len(), Ordering::Relaxed);
643+
future.complete(previous_frame).await;
644+
continue;
645+
}
646+
625647
let frame = hw_decoder::extract_frame_hw_rgba(
626648
&inner.path,
627649
frame_index as _,

backend/src/ffmpeg.rs

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ struct FfprobeStream {
1616
duration: Option<String>,
1717
avg_frame_rate: Option<String>,
1818
r_frame_rate: Option<String>,
19+
nb_read_frames: Option<String>,
1920
nb_frames: Option<String>,
2021
width: Option<u32>,
2122
height: Option<u32>,
@@ -27,7 +28,12 @@ struct FfprobeOutput {
2728
streams: Option<Vec<FfprobeStream>>,
2829
}
2930

30-
fn run_ffprobe(path: &str, select_streams: Option<&str>, entries: &str) -> Result<FfprobeOutput, String> {
31+
fn run_ffprobe(
32+
path: &str,
33+
select_streams: Option<&str>,
34+
entries: &str,
35+
count_frames: bool,
36+
) -> Result<FfprobeOutput, String> {
3137
let ffprobe = bin::ffprobe_path()?;
3238
let mut cmd = Command::new(ffprobe);
3339
cmd.arg("-v")
@@ -36,6 +42,9 @@ fn run_ffprobe(path: &str, select_streams: Option<&str>, entries: &str) -> Resul
3642
.arg("json")
3743
.arg("-show_entries")
3844
.arg(entries);
45+
if count_frames {
46+
cmd.arg("-count_frames");
47+
}
3948
if let Some(select_streams) = select_streams {
4049
cmd.arg("-select_streams").arg(select_streams);
4150
}
@@ -95,7 +104,7 @@ fn parse_ratio(value: Option<&str>) -> Option<f64> {
95104

96105
/// Return video duration in milliseconds using ffprobe metadata.
97106
pub fn probe_video_duration_ms(path: &str) -> Result<u64, String> {
98-
let output = run_ffprobe(path, Some("v:0"), "format=duration:stream=duration")?;
107+
let output = run_ffprobe(path, Some("v:0"), "format=duration:stream=duration", false)?;
99108
let stream_duration = output
100109
.streams
101110
.as_ref()
@@ -111,13 +120,28 @@ pub fn probe_video_duration_ms(path: &str) -> Result<u64, String> {
111120
}
112121

113122
pub fn probe_video_frames(path: &str) -> Result<u64, String> {
114-
let output = run_ffprobe(path, Some("v:0"), "stream=nb_frames,duration,avg_frame_rate")?;
123+
let output = run_ffprobe(
124+
path,
125+
Some("v:0"),
126+
"stream=nb_read_frames,nb_frames,duration,avg_frame_rate",
127+
true,
128+
)?;
115129
let stream = output
116130
.streams
117131
.as_ref()
118132
.and_then(|streams| streams.first())
119133
.ok_or_else(|| "failed to read frames".to_string())?;
120134

135+
if let Some(frames) = stream
136+
.nb_read_frames
137+
.as_deref()
138+
.and_then(|value| value.parse::<u64>().ok())
139+
{
140+
if frames > 0 {
141+
return Ok(frames);
142+
}
143+
}
144+
121145
if let Some(frames) = stream.nb_frames.as_deref().and_then(|value| value.parse::<u64>().ok()) {
122146
if frames > 0 {
123147
return Ok(frames);
@@ -134,7 +158,7 @@ pub fn probe_video_frames(path: &str) -> Result<u64, String> {
134158
}
135159

136160
pub fn probe_video_fps(path: &str) -> Result<f64, String> {
137-
let output = run_ffprobe(path, Some("v:0"), "stream=avg_frame_rate,r_frame_rate")?;
161+
let output = run_ffprobe(path, Some("v:0"), "stream=avg_frame_rate,r_frame_rate", false)?;
138162
let stream = output
139163
.streams
140164
.as_ref()
@@ -149,7 +173,7 @@ pub fn probe_video_fps(path: &str) -> Result<f64, String> {
149173
}
150174

151175
pub fn probe_video_dimensions(path: &str) -> Result<(u32, u32), String> {
152-
let output = run_ffprobe(path, Some("v:0"), "stream=width,height")?;
176+
let output = run_ffprobe(path, Some("v:0"), "stream=width,height", false)?;
153177
let stream = output
154178
.streams
155179
.as_ref()
@@ -170,7 +194,7 @@ pub fn probe_audio_duration_ms(path: &str) -> Result<u64, String> {
170194
// Some containers report bogus global duration; prefer audio stream duration when available.
171195
const MAX_REASONABLE_DURATION_MS: u64 = 1000 * 60 * 60 * 24 * 7; // 7 days
172196

173-
let output = run_ffprobe(path, Some("a:0"), "format=duration:stream=duration")?;
197+
let output = run_ffprobe(path, Some("a:0"), "format=duration:stream=duration", false)?;
174198
let stream_duration = output
175199
.streams
176200
.as_ref()

backend/src/main.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ pub mod ffmpeg;
33
pub mod future;
44
pub mod util;
55

6-
use std::{net::SocketAddr, ops::Bound, sync::atomic::AtomicBool, time::{SystemTime, UNIX_EPOCH}};
6+
use std::{
7+
net::SocketAddr,
8+
ops::Bound,
9+
sync::atomic::AtomicBool,
10+
time::{SystemTime, UNIX_EPOCH},
11+
};
712

813
use axum::{
914
Router,
@@ -28,7 +33,10 @@ use tracing::{error, info};
2833

2934
use crate::{
3035
decoder::{DECODER, DecoderKey, set_max_cache_size},
31-
ffmpeg::{probe_audio_duration_ms, probe_video_dimensions, probe_video_duration_ms, probe_video_fps},
36+
ffmpeg::{
37+
probe_audio_duration_ms, probe_video_dimensions, probe_video_duration_ms, probe_video_fps,
38+
probe_video_frames,
39+
},
3240
util::resolve_path_to_string,
3341
};
3442

@@ -463,6 +471,7 @@ async fn healthz_handler() -> impl IntoResponse {
463471
struct VideoMetadataResponse {
464472
duration_ms: u64,
465473
fps: f64,
474+
frame_count: u64,
466475
width: u32,
467476
height: u32,
468477
}
@@ -476,10 +485,18 @@ async fn video_meta_handler(
476485
probe_video_duration_ms(&resolved_path).map_err(|_| StatusCode::BAD_REQUEST)?;
477486

478487
let fps = probe_video_fps(&resolved_path).map_err(|_| StatusCode::BAD_REQUEST)?;
488+
let frame_count = probe_video_frames(&resolved_path).unwrap_or(0);
479489
let (width, height) =
480490
probe_video_dimensions(&resolved_path).map_err(|_| StatusCode::BAD_REQUEST)?;
481491

482-
let mut resp = Json(VideoMetadataResponse { duration_ms, fps, width, height }).into_response();
492+
let mut resp = Json(VideoMetadataResponse {
493+
duration_ms,
494+
fps,
495+
frame_count,
496+
width,
497+
height,
498+
})
499+
.into_response();
483500
apply_cors(resp.headers_mut());
484501
Ok(resp)
485502
}

src/lib/video/video-render.tsx

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@ import { PROJECT_SETTINGS } from "../../../project/project";
44
import { useCurrentFrame } from "../frame";
55
import { useClipActive, useClipStart, useProvideClipDuration } from "../clip";
66
import { createManualPromise, type ManualPromise } from "../../util/promise";
7-
import { normalizeVideo, video_fps, video_length, type Video, type VideoResolvedTrimProps } from "./video";
7+
import {
8+
normalizeVideo,
9+
video_fps,
10+
video_frame_count,
11+
video_length,
12+
type Video,
13+
type VideoResolvedTrimProps,
14+
} from "./video";
815

916
// Track pending frame draws so headless callers can await completion.
1017
const pendingFramePromises = new Set<Promise<void>>();
@@ -60,6 +67,7 @@ export const VideoCanvasRender = ({ video, style, trimStartFrames = 0, trimEndFr
6067
const reconnectTimerRef = useRef<number | null>(null);
6168
const resolved = useMemo(() => normalizeVideo(video), [video]);
6269
const fps = useMemo(() => video_fps(resolved), [resolved]);
70+
const sourceFrameCount = useMemo(() => video_frame_count(resolved), [resolved]);
6371
const rawDurationFrames = useMemo(() => video_length(resolved), [resolved]);
6472
const durationFrames = Math.max(0, rawDurationFrames - trimStartFrames - trimEndFrames);
6573
useProvideClipDuration(durationFrames);
@@ -81,6 +89,10 @@ export const VideoCanvasRender = ({ video, style, trimStartFrames = 0, trimEndFr
8189

8290
const resize = () => {
8391
const rect = canvas.getBoundingClientRect();
92+
if (rect.width <= 0 || rect.height <= 0) {
93+
// Hidden clips report 0x0; keep previous size to avoid requesting 1x1 frames.
94+
return;
95+
}
8496
const dpr = window.devicePixelRatio || 1;
8597
const nextWidth = Math.max(1, Math.round(rect.width * dpr));
8698
const nextHeight = Math.max(1, Math.round(rect.height * dpr));
@@ -139,8 +151,8 @@ export const VideoCanvasRender = ({ video, style, trimStartFrames = 0, trimEndFr
139151

140152
const req = {
141153
video: resolved.path,
142-
width: canvasSizeRef.current.width,
143-
height: canvasSizeRef.current.height,
154+
width: canvasSizeRef.current.width > 1 ? canvasSizeRef.current.width : PROJECT_SETTINGS.width,
155+
height: canvasSizeRef.current.height > 1 ? canvasSizeRef.current.height : PROJECT_SETTINGS.height,
144156
frame: playbackFrame,
145157
};
146158

@@ -154,15 +166,27 @@ export const VideoCanvasRender = ({ video, style, trimStartFrames = 0, trimEndFr
154166
const clampedFrame =
155167
maxFrame !== undefined ? Math.min(Math.max(frame, 0), maxFrame) : Math.max(frame, 0);
156168

157-
const sourceStart = trimStartFrames;
158-
const sourceEnd = Math.max(sourceStart, rawDurationFrames - trimEndFrames - 1);
169+
const sourceStart =
170+
fps > 0
171+
? Math.floor((trimStartFrames * fps) / PROJECT_SETTINGS.fps)
172+
: trimStartFrames;
173+
const sourceTrimEnd =
174+
fps > 0
175+
? Math.floor((trimEndFrames * fps) / PROJECT_SETTINGS.fps)
176+
: trimEndFrames;
177+
const estimatedSourceFrames =
178+
fps > 0
179+
? Math.max(0, Math.round((rawDurationFrames * fps) / PROJECT_SETTINGS.fps))
180+
: rawDurationFrames;
181+
const sourceTotalFrames = sourceFrameCount > 0 ? sourceFrameCount : estimatedSourceFrames;
182+
const sourceEnd = Math.max(sourceStart, sourceTotalFrames - sourceTrimEnd - 1);
159183

160184
requestedFrameRef.current = clampedFrame;
161185

162186
const playbackFrameRaw =
163187
fps > 0
164-
? Math.round(((clampedFrame + sourceStart) * fps) / PROJECT_SETTINGS.fps)
165-
: clampedFrame + sourceStart;
188+
? Math.floor(((clampedFrame + trimStartFrames) * fps) / PROJECT_SETTINGS.fps)
189+
: clampedFrame + trimStartFrames;
166190
const playbackFrame = Math.min(Math.max(playbackFrameRaw, sourceStart), sourceEnd);
167191

168192
const alreadyDrawn =
@@ -182,7 +206,15 @@ export const VideoCanvasRender = ({ video, style, trimStartFrames = 0, trimEndFr
182206

183207
sendPlaybackFrameRequest(playbackFrame);
184208
},
185-
[durationFrames, fps, rawDurationFrames, sendPlaybackFrameRequest, trimEndFrames, trimStartFrames],
209+
[
210+
durationFrames,
211+
fps,
212+
rawDurationFrames,
213+
sendPlaybackFrameRequest,
214+
sourceFrameCount,
215+
trimEndFrames,
216+
trimStartFrames,
217+
],
186218
);
187219

188220
useEffect(() => {
@@ -271,10 +303,8 @@ export const VideoCanvasRender = ({ video, style, trimStartFrames = 0, trimEndFr
271303
pending?.projectFrame ??
272304
Math.max(
273305
0,
274-
Math.round(
275-
((frameIndex - trimStartFrames) * PROJECT_SETTINGS.fps) /
276-
Math.max(1, fps || PROJECT_SETTINGS.fps),
277-
),
306+
Math.round((frameIndex * PROJECT_SETTINGS.fps) / Math.max(1, fps || PROJECT_SETTINGS.fps)) -
307+
trimStartFrames,
278308
);
279309

280310
if (pending) {
@@ -310,6 +340,13 @@ export const VideoCanvasRender = ({ video, style, trimStartFrames = 0, trimEndFr
310340
};
311341
}, [rejectPendingRequests, resolveWaiters, sendFrameRequest, sendPlaybackFrameRequest, visible]);
312342

343+
useEffect(() => {
344+
if (visible) return;
345+
// Re-entering a clip must not reuse stale last-drawn markers.
346+
lastDrawnFrameRef.current = null;
347+
requestedFrameRef.current = null;
348+
}, [visible]);
349+
313350
useEffect(() => {
314351
if (!visible) return;
315352
sendFrameRequest(currentFrame);
@@ -323,11 +360,15 @@ export const VideoCanvasRender = ({ video, style, trimStartFrames = 0, trimEndFr
323360
}
324361
const startOffset = clipStart ?? 0
325362
const relativeFrame = frame - startOffset
326-
if (relativeFrame < 0 || durationFrames <= 0) return
363+
if (relativeFrame < 0 || durationFrames <= 0) {
364+
return
365+
}
327366
const maxFrame = Math.max(0, durationFrames - 1)
328367
const clampedFrame = Math.min(Math.max(relativeFrame, 0), maxFrame)
329368
const lastDrawn = lastDrawnFrameRef.current
330-
if (lastDrawn != null && lastDrawn >= clampedFrame) return
369+
if (lastDrawn != null && lastDrawn >= clampedFrame) {
370+
return
371+
}
331372
await createOrGetFramePromise(clampedFrame).promise
332373
}
333374

src/lib/video/video.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ const buildMetaUrl = (video: Video) => {
7070
type VideoMeta = {
7171
duration_ms: number
7272
fps: number
73+
frame_count: number
7374
width: number
7475
height: number
7576
}
@@ -81,7 +82,7 @@ const fetchVideoMetaSync = (video: Video): VideoMeta => {
8182
return videoMetaCache.get(video.path)!
8283
}
8384

84-
const fallback: VideoMeta = { duration_ms: 0, fps: 0, width: 0, height: 0 }
85+
const fallback: VideoMeta = { duration_ms: 0, fps: 0, frame_count: 0, width: 0, height: 0 }
8586

8687
try {
8788
const xhr = new XMLHttpRequest()
@@ -93,6 +94,8 @@ const fetchVideoMetaSync = (video: Video): VideoMeta => {
9394
const meta: VideoMeta = {
9495
duration_ms: typeof payload.duration_ms === "number" ? Math.max(0, payload.duration_ms) : 0,
9596
fps: typeof payload.fps === "number" ? payload.fps : 0,
97+
frame_count:
98+
typeof payload.frame_count === "number" ? Math.max(0, Math.round(payload.frame_count)) : 0,
9699
width: typeof payload.width === "number" ? Math.max(0, Math.round(payload.width)) : 0,
97100
height: typeof payload.height === "number" ? Math.max(0, Math.round(payload.height)) : 0,
98101
}
@@ -120,6 +123,9 @@ const fetchVideoMetaSync = (video: Video): VideoMeta => {
120123
export const video_length = (video: Video | string): number => {
121124
const resolved = normalizeVideo(video)
122125
const meta = fetchVideoMetaSync(resolved)
126+
if (meta.frame_count > 0 && meta.fps > 0) {
127+
return Math.round((meta.frame_count * PROJECT_SETTINGS.fps) / meta.fps)
128+
}
123129
const seconds = meta.duration_ms > 0 ? meta.duration_ms / 1000 : 0
124130
return Math.round(seconds * PROJECT_SETTINGS.fps)
125131
}
@@ -140,6 +146,12 @@ export const video_fps = (video: Video | string): number => {
140146
return meta.fps
141147
}
142148

149+
export const video_frame_count = (video: Video | string): number => {
150+
const resolved = normalizeVideo(video)
151+
const meta = fetchVideoMetaSync(resolved)
152+
return meta.frame_count
153+
}
154+
143155
export type VideoDimensions = {
144156
width: number
145157
height: number

0 commit comments

Comments
 (0)