Skip to content

Commit a4f3992

Browse files
committed
fix video aspect retio issue
1 parent 8072c66 commit a4f3992

File tree

4 files changed

+96
-54
lines changed

4 files changed

+96
-54
lines changed

backend/src/ffmpeg.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ struct FfprobeStream {
1717
avg_frame_rate: Option<String>,
1818
r_frame_rate: Option<String>,
1919
nb_frames: Option<String>,
20+
width: Option<u32>,
21+
height: Option<u32>,
2022
}
2123

2224
#[derive(Debug, Deserialize)]
@@ -146,6 +148,23 @@ pub fn probe_video_fps(path: &str) -> Result<f64, String> {
146148
Ok(fps)
147149
}
148150

151+
pub fn probe_video_dimensions(path: &str) -> Result<(u32, u32), String> {
152+
let output = run_ffprobe(path, Some("v:0"), "stream=width,height")?;
153+
let stream = output
154+
.streams
155+
.as_ref()
156+
.and_then(|streams| streams.first())
157+
.ok_or_else(|| "failed to read dimensions".to_string())?;
158+
159+
let width = stream.width.unwrap_or(0);
160+
let height = stream.height.unwrap_or(0);
161+
if width > 0 && height > 0 {
162+
Ok((width, height))
163+
} else {
164+
Err("failed to read dimensions".to_string())
165+
}
166+
}
167+
149168
/// Return audio duration in milliseconds using ffprobe metadata.
150169
pub fn probe_audio_duration_ms(path: &str) -> Result<u64, String> {
151170
// Some containers report bogus global duration; prefer audio stream duration when available.

backend/src/main.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use tracing::{error, info};
2828

2929
use crate::{
3030
decoder::{DECODER, DecoderKey, set_max_cache_size},
31-
ffmpeg::{probe_audio_duration_ms, probe_video_duration_ms, probe_video_fps},
31+
ffmpeg::{probe_audio_duration_ms, probe_video_dimensions, probe_video_duration_ms, probe_video_fps},
3232
util::resolve_path_to_string,
3333
};
3434

@@ -429,6 +429,8 @@ async fn healthz_handler() -> impl IntoResponse {
429429
struct VideoMetadataResponse {
430430
duration_ms: u64,
431431
fps: f64,
432+
width: u32,
433+
height: u32,
432434
}
433435

434436
async fn video_meta_handler(
@@ -440,8 +442,10 @@ async fn video_meta_handler(
440442
probe_video_duration_ms(&resolved_path).map_err(|_| StatusCode::BAD_REQUEST)?;
441443

442444
let fps = probe_video_fps(&resolved_path).map_err(|_| StatusCode::BAD_REQUEST)?;
445+
let (width, height) =
446+
probe_video_dimensions(&resolved_path).map_err(|_| StatusCode::BAD_REQUEST)?;
443447

444-
let mut resp = Json(VideoMetadataResponse { duration_ms, fps }).into_response();
448+
let mut resp = Json(VideoMetadataResponse { duration_ms, fps, width, height }).into_response();
445449
apply_cors(resp.headers_mut());
446450
Ok(resp)
447451
}

src/lib/video/video-render.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export type VideoCanvasRenderProps = {
5252
export const VideoCanvasRender = ({ video, style, trimStartFrames = 0, trimEndFrames = 0 }: VideoCanvasRenderProps) => {
5353
const canvasRef = useRef<HTMLCanvasElement | null>(null);
5454
const wsRef = useRef<WebSocket | null>(null);
55+
const canvasSizeRef = useRef({ width: PROJECT_SETTINGS.width, height: PROJECT_SETTINGS.height });
5556
const pendingMapRef = useRef<Map<number, { manual: ManualPromise<void>; projectFrame: number }>>(new Map());
5657
const waitersRef = useRef<Map<number, ManualPromise<void>>>(new Map());
5758
const lastDrawnFrameRef = useRef<number | null>(null);
@@ -87,6 +88,7 @@ export const VideoCanvasRender = ({ video, style, trimStartFrames = 0, trimEndFr
8788
canvas.width = nextWidth;
8889
canvas.height = nextHeight;
8990
}
91+
canvasSizeRef.current = { width: canvas.width, height: canvas.height };
9092
};
9193

9294
resize();
@@ -126,7 +128,6 @@ export const VideoCanvasRender = ({ video, style, trimStartFrames = 0, trimEndFr
126128
if (projectFrame > prev) {
127129
lastDrawnFrameRef.current = projectFrame;
128130
}
129-
const hadWaiter = waitersRef.current.has(projectFrame);
130131
createOrGetFramePromise(projectFrame).resolve()
131132
}, []);
132133

@@ -138,8 +139,8 @@ export const VideoCanvasRender = ({ video, style, trimStartFrames = 0, trimEndFr
138139

139140
const req = {
140141
video: resolved.path,
141-
width: PROJECT_SETTINGS.width,
142-
height: PROJECT_SETTINGS.height,
142+
width: canvasSizeRef.current.width,
143+
height: canvasSizeRef.current.height,
143144
frame: playbackFrame,
144145
};
145146

src/lib/video/video.tsx

Lines changed: 67 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -66,48 +66,62 @@ const buildMetaUrl = (video: Video) => {
6666
return url.toString();
6767
}
6868

69-
const videoLengthCache = new Map<string, number>()
69+
type VideoMeta = {
70+
duration_ms: number
71+
fps: number
72+
width: number
73+
height: number
74+
}
7075

71-
/**
72-
* Returns video length in frames (project FPS).
73-
*
74-
* 動画の長さをフレーム数で返します。
75-
*
76-
* @example
77-
* ```ts
78-
* const frames = video_length("assets/demo.mp4")
79-
* ```
80-
*/
81-
export const video_length = (video: Video | string): number => {
82-
const resolved = normalizeVideo(video)
76+
const videoMetaCache = new Map<string, VideoMeta>()
8377

84-
if (videoLengthCache.has(resolved.path)) {
85-
return videoLengthCache.get(resolved.path)!
78+
const fetchVideoMetaSync = (video: Video): VideoMeta => {
79+
if (videoMetaCache.has(video.path)) {
80+
return videoMetaCache.get(video.path)!
8681
}
8782

83+
const fallback: VideoMeta = { duration_ms: 0, fps: 0, width: 0, height: 0 }
84+
8885
try {
8986
const xhr = new XMLHttpRequest()
90-
xhr.open("GET", buildMetaUrl(resolved), false) // 同期リクエストで初期ロード用途
87+
xhr.open("GET", buildMetaUrl(video), false) // 同期リクエストで初期ロード用途
9188
xhr.send()
9289

9390
if (xhr.status >= 200 && xhr.status < 300) {
94-
const payload = JSON.parse(xhr.responseText) as { duration_ms?: number, fps?: number }
95-
const seconds = typeof payload.duration_ms === "number"
96-
? Math.max(0, payload.duration_ms) / 1000
97-
: 0
98-
const frames = Math.round(seconds * PROJECT_SETTINGS.fps)
99-
videoLengthCache.set(resolved.path, frames)
100-
return frames
91+
const payload = JSON.parse(xhr.responseText) as Partial<VideoMeta>
92+
const meta: VideoMeta = {
93+
duration_ms: typeof payload.duration_ms === "number" ? Math.max(0, payload.duration_ms) : 0,
94+
fps: typeof payload.fps === "number" ? payload.fps : 0,
95+
width: typeof payload.width === "number" ? Math.max(0, Math.round(payload.width)) : 0,
96+
height: typeof payload.height === "number" ? Math.max(0, Math.round(payload.height)) : 0,
97+
}
98+
videoMetaCache.set(video.path, meta)
99+
return meta
101100
}
102101
} catch (error) {
103-
console.error("video_length(): failed to fetch metadata", error)
102+
console.error("fetchVideoMetaSync(): failed to fetch metadata", error)
104103
}
105104

106-
videoLengthCache.set(resolved.path, 0)
107-
return 0
105+
videoMetaCache.set(video.path, fallback)
106+
return fallback
108107
}
109108

110-
const videoFpsCache = new Map<string, number>()
109+
/**
110+
* Returns video length in frames (project FPS).
111+
*
112+
* 動画の長さをフレーム数で返します。
113+
*
114+
* @example
115+
* ```ts
116+
* const frames = video_length("assets/demo.mp4")
117+
* ```
118+
*/
119+
export const video_length = (video: Video | string): number => {
120+
const resolved = normalizeVideo(video)
121+
const meta = fetchVideoMetaSync(resolved)
122+
const seconds = meta.duration_ms > 0 ? meta.duration_ms / 1000 : 0
123+
return Math.round(seconds * PROJECT_SETTINGS.fps)
124+
}
111125

112126
/**
113127
* Returns the source video FPS.
@@ -121,28 +135,19 @@ const videoFpsCache = new Map<string, number>()
121135
*/
122136
export const video_fps = (video: Video | string): number => {
123137
const resolved = normalizeVideo(video)
138+
const meta = fetchVideoMetaSync(resolved)
139+
return meta.fps
140+
}
124141

125-
if (videoFpsCache.has(resolved.path)) {
126-
return videoFpsCache.get(resolved.path)!
127-
}
128-
129-
try {
130-
const xhr = new XMLHttpRequest()
131-
xhr.open("GET", buildMetaUrl(resolved), false) // 同期リクエストで初期ロード用途
132-
xhr.send()
133-
134-
if (xhr.status >= 200 && xhr.status < 300) {
135-
const payload = JSON.parse(xhr.responseText) as { duration_ms?: number, fps?: number }
136-
const fps = typeof payload.fps === "number" ? payload.fps : 0
137-
videoFpsCache.set(resolved.path, fps)
138-
return fps
139-
}
140-
} catch (error) {
141-
console.error("video_fps(): failed to fetch metadata", error)
142-
}
142+
export type VideoDimensions = {
143+
width: number
144+
height: number
145+
}
143146

144-
videoFpsCache.set(resolved.path, 0)
145-
return 0
147+
export const video_dimensions = (video: Video | string): VideoDimensions => {
148+
const resolved = normalizeVideo(video)
149+
const meta = fetchVideoMetaSync(resolved)
150+
return { width: meta.width, height: meta.height }
146151
}
147152

148153
/**
@@ -175,6 +180,19 @@ export const Video = ({ video, style, trim }: VideoProps) => {
175180
const id = useId()
176181
const clipRange = useClipRange()
177182
const resolvedVideo = useMemo(() => normalizeVideo(video), [video])
183+
const resolvedStyle = useMemo(() => {
184+
if (style?.aspectRatio != null) {
185+
return style
186+
}
187+
const { width, height } = video_dimensions(resolvedVideo)
188+
if (width <= 0 || height <= 0) {
189+
return style
190+
}
191+
return {
192+
...style,
193+
aspectRatio: `${width} / ${height}`,
194+
}
195+
}, [resolvedVideo, style])
178196
const rawDurationFrames = useMemo(() => video_length(resolvedVideo), [resolvedVideo])
179197
const { trimStartFrames, trimEndFrames } = useMemo(
180198
() =>
@@ -211,7 +229,7 @@ export const Video = ({ video, style, trim }: VideoProps) => {
211229
return (
212230
<VideoCanvasRender
213231
video={video}
214-
style={style}
232+
style={resolvedStyle}
215233
trimStartFrames={trimStartFrames}
216234
trimEndFrames={trimEndFrames}
217235
/>
@@ -220,7 +238,7 @@ export const Video = ({ video, style, trim }: VideoProps) => {
220238
return (
221239
<VideoCanvas
222240
video={video}
223-
style={style}
241+
style={resolvedStyle}
224242
trimStartFrames={trimStartFrames}
225243
trimEndFrames={trimEndFrames}
226244
/>

0 commit comments

Comments
 (0)