@@ -14,7 +14,6 @@ use serde::{Deserialize, Serialize};
1414use std:: path:: PathBuf ;
1515use std:: sync:: atomic:: { AtomicBool , AtomicUsize , Ordering } ;
1616use std:: sync:: { Arc , OnceLock } ;
17- use tempfile:: TempDir ;
1817
1918use crate :: ffmpeg:: { AudioPlanResolved , SegmentWriter , mux_audio_plan_into_mp4} ;
2019
@@ -30,6 +29,7 @@ struct CancelResponse {
3029}
3130
3231static CHROMIUM_EXECUTABLE : OnceLock < Option < PathBuf > > = OnceLock :: new ( ) ;
32+ const FRAME_DIRECTORY : & str = "frames" ;
3333
3434fn parse_bool_token ( value : & str ) -> Option < bool > {
3535 match value. trim ( ) . to_ascii_lowercase ( ) . as_str ( ) {
@@ -60,13 +60,12 @@ fn resolve_chromium_executable() -> Option<PathBuf> {
6060}
6161
6262async fn spawn_browser_instance (
63- profile_id : usize ,
63+ profile_dir : PathBuf ,
6464 width : u32 ,
6565 height : u32 ,
6666) -> Result < ( Browser , Handler ) , Box < dyn std:: error:: Error > > {
67- // 一時ディレクトリをブラウザプロファイルとして使う
68- let tmp = TempDir :: new ( ) ?; // ライフタイム管理は適宜
69- let user_data_dir: PathBuf = tmp. path ( ) . join ( format ! ( "profile-{}" , profile_id) ) ;
67+ // Keep profile dir alive for the whole worker lifetime.
68+ std:: fs:: create_dir_all ( & profile_dir) ?;
7069
7170 let mut builder = BrowserConfig :: builder ( )
7271 . new_headless_mode ( )
@@ -79,8 +78,10 @@ async fn spawn_browser_instance(
7978 has_touch : false ,
8079 } )
8180 //.arg("--use-angle=swiftshader")
81+ . arg ( "--no-first-run" )
82+ . arg ( "--no-default-browser-check" )
8283 . request_timeout ( Duration :: from_hours ( 24 ) )
83- . user_data_dir ( user_data_dir ) ; // ★ インスタンスごとに別のディレクトリ
84+ . user_data_dir ( profile_dir ) ;
8485
8586 if let Some ( path) = resolve_chromium_executable ( ) {
8687 builder = builder. chrome_executable ( path) ;
@@ -298,14 +299,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
298299 . unwrap_or_else ( |_| "http://localhost:5174/render" . to_string ( ) ) ;
299300
300301 let mut tasks = FuturesUnordered :: new ( ) ;
302+ let mut launched_worker_ids = Vec :: new ( ) ;
301303
302- static DIRECTORY : & ' static str = "frames" ;
303304 let output_path =
304305 std:: env:: var ( "RENDER_OUTPUT_PATH" ) . unwrap_or_else ( |_| "output.mp4" . to_string ( ) ) ;
305306 let output_path = PathBuf :: from ( output_path) ;
306307
307- tokio:: fs:: remove_dir_all ( DIRECTORY ) . await . ok ( ) ;
308- tokio:: fs:: create_dir ( DIRECTORY ) . await ?;
308+ tokio:: fs:: remove_dir_all ( FRAME_DIRECTORY ) . await . ok ( ) ;
309+ tokio:: fs:: create_dir ( FRAME_DIRECTORY ) . await ?;
309310
310311 let start = Instant :: now ( ) ;
311312
@@ -326,6 +327,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
326327 }
327328
328329 for ( worker_id, ( start, end) ) in ranges. into_iter ( ) . enumerate ( ) {
330+ launched_worker_ids. push ( worker_id) ;
329331 let encode_clone = encode. clone ( ) ;
330332 let preset_clone = preset. clone ( ) ;
331333 let ffmpeg_threads_clone = ffmpeg_threads;
@@ -335,13 +337,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
335337 let completed_clone = completed. clone ( ) ;
336338 let is_canceled_clone = is_canceled. clone ( ) ;
337339 tasks. push ( tokio:: spawn ( async move {
338- let ( mut browser, mut handler) = spawn_browser_instance ( worker_id, width, height)
340+ let profile_dir =
341+ PathBuf :: from ( format ! ( "{}/profiles/profile-{:03}" , FRAME_DIRECTORY , worker_id) ) ;
342+
343+ let ( mut browser, mut handler) = spawn_browser_instance ( profile_dir, width, height)
339344 . await
340- . unwrap ( ) ;
345+ . map_err ( |error| format ! ( "failed to launch browser worker {worker_id}: {error}" ) ) ? ;
341346
342347 tokio:: spawn ( async move { while handler. next ( ) . await . is_some ( ) { } } ) ;
343348
344- let out = format ! ( "{}/segment-{worker_id:03}.mp4" , DIRECTORY ) ;
349+ let out = format ! ( "{}/segment-{worker_id:03}.mp4" , FRAME_DIRECTORY ) ;
345350
346351 let mut writer = SegmentWriter :: new (
347352 & out,
@@ -356,10 +361,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
356361 ffmpeg_low_memory_clone,
357362 )
358363 . await
359- . unwrap ( ) ;
364+ . map_err ( |error| format ! ( "worker {worker_id}: failed to create ffmpeg writer: {error}" ) ) ? ;
360365
361- let page = browser. new_page ( page_url) . await . unwrap ( ) ;
362- page. wait_for_navigation ( ) . await . unwrap ( ) ;
366+ let page = browser
367+ . new_page ( page_url)
368+ . await
369+ . map_err ( |error| format ! ( "worker {worker_id}: failed to open page: {error}" ) ) ?;
370+ page. wait_for_navigation ( )
371+ . await
372+ . map_err ( |error| format ! ( "worker {worker_id}: navigation failed: {error}" ) ) ?;
363373 wait_for_frame_api ( & page) . await ;
364374 wait_for_animation_ready ( & page) . await ;
365375 wait_for_draw_text_ready ( & page) . await ;
@@ -379,7 +389,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
379389 "# ,
380390 frame
381391 ) ;
382- page. evaluate ( js) . await . unwrap ( ) ;
392+ page. evaluate ( js)
393+ . await
394+ . map_err ( |error| format ! ( "worker {worker_id}: setFrame eval failed: {error}" ) ) ?;
383395
384396 wait_for_next_frame ( & page) . await ;
385397
@@ -398,7 +410,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
398410 "# ,
399411 frame
400412 ) ;
401- page. evaluate ( script) . await . unwrap ( ) ;
413+ page. evaluate ( script)
414+ . await
415+ . map_err ( |error| format ! ( "worker {worker_id}: waitCanvasFrame eval failed: {error}" ) ) ?;
402416
403417 wait_for_images_ready ( & page) . await ;
404418 wait_for_webgl_frame ( & page, frame) . await ;
@@ -411,9 +425,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
411425 . build ( ) ,
412426 )
413427 . await
414- . unwrap ( ) ;
428+ . map_err ( |error| format ! ( "worker {worker_id}: screenshot failed: {error}" ) ) ? ;
415429
416- writer. write_png_frame ( & bytes) . await . unwrap ( ) ;
430+ writer
431+ . write_png_frame ( & bytes)
432+ . await
433+ . map_err ( |error| format ! ( "worker {worker_id}: ffmpeg write failed: {error}" ) ) ?;
417434
418435 completed_clone. fetch_add ( 1 , Ordering :: Relaxed ) ;
419436
@@ -422,18 +439,36 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
422439 }
423440 }
424441
425- writer. finish ( ) . await . unwrap ( ) ;
442+ writer
443+ . finish ( )
444+ . await
445+ . map_err ( |error| format ! ( "worker {worker_id}: ffmpeg finalize failed: {error}" ) ) ?;
446+
447+ browser
448+ . close ( )
449+ . await
450+ . map_err ( |error| format ! ( "worker {worker_id}: browser close failed: {error}" ) ) ?;
426451
427- browser . close ( ) . await . unwrap ( ) ;
452+ Ok :: < ( ) , String > ( ( ) )
428453 } ) ) ;
429454 }
430455
431- while let Some ( _) = tasks. next ( ) . await { }
456+ while let Some ( task_result) = tasks. next ( ) . await {
457+ match task_result {
458+ Ok ( Ok ( ( ) ) ) => { }
459+ Ok ( Err ( worker_error) ) => return Err ( Box :: < dyn std:: error:: Error > :: from ( worker_error) ) ,
460+ Err ( join_error) => {
461+ return Err ( Box :: < dyn std:: error:: Error > :: from ( format ! (
462+ "render worker panicked: {join_error}"
463+ ) ) ) ;
464+ }
465+ }
466+ }
432467
433468 let mut segs = Vec :: new ( ) ;
434469
435- for worker_id in 0 ..worker_count + if remainder > 0 { 1 } else { 0 } {
436- let path = PathBuf :: from ( format ! ( "{}/segment-{worker_id:03}.mp4" , DIRECTORY ) ) ;
470+ for worker_id in launched_worker_ids {
471+ let path = PathBuf :: from ( format ! ( "{}/segment-{worker_id:03}.mp4" , FRAME_DIRECTORY ) ) ;
437472 if tokio:: fs:: metadata ( & path) . await . is_ok ( ) {
438473 segs. push ( path) ;
439474 }
0 commit comments