@@ -12,6 +12,10 @@ const encodeOptions = [
1212 { value : "H264" , label : "H264 (software)" } ,
1313 { value : "H265" , label : "H265 (software)" } ,
1414] ;
15+ const loudnessOptions = [
16+ { value : "off" , label : "Default" , help : "Keep the mix as-is." } ,
17+ { value : "youtube" , label : "YouTube" , help : "Target -14 LUFS, -1 dBTP." } ,
18+ ] ;
1519
1620const containerStyle : CSSProperties = {
1721 padding : 20 ,
@@ -35,6 +39,22 @@ const inputStyle: CSSProperties = {
3539 background : "#0f172a" ,
3640 color : "#e5e7eb" ,
3741} ;
42+ const sectionStyle : CSSProperties = {
43+ background : "#0f172a" ,
44+ border : "1px solid #1f2a3c" ,
45+ borderRadius : 12 ,
46+ padding : 12 ,
47+ display : "flex" ,
48+ flexDirection : "column" ,
49+ gap : 10 ,
50+ } ;
51+ const sectionTitleStyle : CSSProperties = {
52+ fontSize : 11 ,
53+ textTransform : "uppercase" ,
54+ letterSpacing : "0.08em" ,
55+ color : "#94a3b8" ,
56+ fontWeight : 600 ,
57+ } ;
3858
3959export const RenderSettingsPage = ( ) => {
4060 const [ width , setWidth ] = useState ( PROJECT_SETTINGS . width ?? 1920 ) ;
@@ -49,6 +69,7 @@ export const RenderSettingsPage = () => {
4969 } ) ;
5070 const [ encode , setEncode ] = useState < "H264" | "H265" > ( "H264" ) ;
5171 const [ preset , setPreset ] = useState ( "medium" ) ;
72+ const [ loudness , setLoudness ] = useState < "off" | "youtube" > ( "off" ) ;
5273 const [ cacheGiB , setCacheGiB ] = useState ( 4 ) ;
5374 const [ status , setStatus ] = useState < string | null > ( null ) ;
5475 const [ busy , setBusy ] = useState ( false ) ;
@@ -120,13 +141,21 @@ export const RenderSettingsPage = () => {
120141 // ignore; still try to start render
121142 }
122143 try {
144+ const audioPlanPayload : {
145+ fps : number ;
146+ segments : typeof audioSegments ;
147+ loudness ?: "youtube" ;
148+ } = {
149+ fps : Number ( fps ) ,
150+ segments : audioSegments ,
151+ } ;
152+ if ( loudness === "youtube" ) {
153+ audioPlanPayload . loudness = "youtube" ;
154+ }
123155 await fetch ( "http://127.0.0.1:3000/render_audio_plan" , {
124156 method : "POST" ,
125157 headers : { "Content-Type" : "application/json" } ,
126- body : JSON . stringify ( {
127- fps : Number ( fps ) ,
128- segments : audioSegments ,
129- } ) ,
158+ body : JSON . stringify ( audioPlanPayload ) ,
130159 } ) ;
131160 } catch ( _error ) {
132161 // ignore; still try to start render
@@ -255,33 +284,71 @@ export const RenderSettingsPage = () => {
255284 </ div >
256285 </ div >
257286
258- < div style = { { marginTop : 16 , display : "flex" , flexDirection : "column" , gap : 10 } } >
259- < div style = { { display : "flex" , gap : 12 , flexWrap : "wrap" } } >
260- { encodeOptions . map ( ( option ) => (
261- < label
262- key = { option . value }
263- style = { {
264- display : "inline-flex" ,
265- alignItems : "center" ,
266- gap : 8 ,
267- padding : "10px 12px" ,
268- borderRadius : 10 ,
269- border : "1px solid #1f2a3c" ,
270- background : encode === option . value ? "linear-gradient(90deg, #1f2937, #0f172a)" : "#0f172a" ,
271- cursor : "pointer" ,
272- userSelect : "none" ,
273- } }
274- >
275- < input
276- type = "radio"
277- value = { option . value }
278- checked = { encode === option . value }
279- onChange = { ( ) => setEncode ( option . value as "H264" | "H265" ) }
280- style = { { accentColor : "#5bd5ff" } }
281- />
282- { option . label }
283- </ label >
284- ) ) }
287+ < div style = { { marginTop : 16 , display : "flex" , flexDirection : "column" , gap : 12 } } >
288+ < div style = { sectionStyle } >
289+ < div style = { sectionTitleStyle } > Encoding</ div >
290+ < div style = { { display : "flex" , gap : 12 , flexWrap : "wrap" } } >
291+ { encodeOptions . map ( ( option ) => (
292+ < label
293+ key = { option . value }
294+ style = { {
295+ display : "inline-flex" ,
296+ alignItems : "center" ,
297+ gap : 8 ,
298+ padding : "10px 12px" ,
299+ borderRadius : 10 ,
300+ border : "1px solid #1f2a3c" ,
301+ background : encode === option . value ? "linear-gradient(90deg, #1f2937, #0f172a)" : "#0f172a" ,
302+ cursor : "pointer" ,
303+ userSelect : "none" ,
304+ } }
305+ >
306+ < input
307+ type = "radio"
308+ value = { option . value }
309+ checked = { encode === option . value }
310+ onChange = { ( ) => setEncode ( option . value as "H264" | "H265" ) }
311+ style = { { accentColor : "#5bd5ff" } }
312+ />
313+ { option . label }
314+ </ label >
315+ ) ) }
316+ </ div >
317+ </ div >
318+ < div style = { sectionStyle } >
319+ < div style = { sectionTitleStyle } > Audio</ div >
320+ < div style = { { fontSize : 12 , color : "#cbd5e1" } } > Loudness</ div >
321+ < div style = { { display : "flex" , gap : 12 , flexWrap : "wrap" } } >
322+ { loudnessOptions . map ( ( option ) => (
323+ < label
324+ key = { option . value }
325+ style = { {
326+ display : "inline-flex" ,
327+ alignItems : "center" ,
328+ gap : 10 ,
329+ padding : "10px 12px" ,
330+ borderRadius : 10 ,
331+ border : "1px solid #1f2a3c" ,
332+ background : loudness === option . value ? "linear-gradient(90deg, #1f2937, #0f172a)" : "#0f172a" ,
333+ cursor : "pointer" ,
334+ userSelect : "none" ,
335+ minWidth : 200 ,
336+ } }
337+ >
338+ < input
339+ type = "radio"
340+ value = { option . value }
341+ checked = { loudness === option . value }
342+ onChange = { ( ) => setLoudness ( option . value as "off" | "youtube" ) }
343+ style = { { accentColor : "#5bd5ff" } }
344+ />
345+ < div style = { { display : "flex" , flexDirection : "column" , gap : 2 } } >
346+ < span style = { { fontSize : 12 , fontWeight : 600 , color : "#e5e7eb" } } > { option . label } </ span >
347+ < span style = { { fontSize : 11 , color : "#94a3b8" } } > { option . help } </ span >
348+ </ div >
349+ </ label >
350+ ) ) }
351+ </ div >
285352 </ div >
286353
287354 < div
0 commit comments