@@ -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 */
122136export 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