@@ -547,6 +547,197 @@ pub enum RecordingAction {
547547 UpgradeRequired ,
548548}
549549
550+ const MICROPHONE_INPUT_PROBE_TIMEOUT : Duration = Duration :: from_millis ( 1500 ) ;
551+ const CAMERA_INPUT_PROBE_TIMEOUT : Duration = Duration :: from_millis ( 1500 ) ;
552+
553+ fn camera_id_label ( id : & camera:: DeviceOrModelID ) -> String {
554+ match id {
555+ camera:: DeviceOrModelID :: DeviceID ( device_id) => device_id. clone ( ) ,
556+ camera:: DeviceOrModelID :: ModelID ( model_id) => format ! ( "{model_id:?}" ) ,
557+ }
558+ }
559+
560+ fn camera_lock_matches_id (
561+ lock : & CameraFeedLock ,
562+ selected_id : & camera:: DeviceOrModelID ,
563+ ) -> bool {
564+ let camera_info = lock. camera_info ( ) ;
565+ match selected_id {
566+ camera:: DeviceOrModelID :: DeviceID ( device_id) => camera_info. device_id ( ) == device_id,
567+ camera:: DeviceOrModelID :: ModelID ( model_id) => camera_info. model_id ( ) == Some ( model_id) ,
568+ }
569+ }
570+
571+ async fn initialize_selected_camera (
572+ camera_feed : & kameo:: actor:: ActorRef < camera:: CameraFeed > ,
573+ id : & camera:: DeviceOrModelID ,
574+ settings : Option < camera:: CameraDeviceSettings > ,
575+ ) -> anyhow:: Result < ( ) > {
576+ let label = camera_id_label ( id) ;
577+ let ready = camera_feed
578+ . ask ( camera:: SetInput {
579+ id : id. clone ( ) ,
580+ settings,
581+ } )
582+ . await
583+ . map_err ( |err| anyhow ! ( "Failed to initialize selected camera '{label}': {err}" ) ) ?;
584+
585+ ready. await . map_err ( |err| match err {
586+ camera:: SetInputError :: DeviceNotFound => {
587+ anyhow ! ( "Selected camera '{label}' is no longer available" )
588+ }
589+ err => anyhow ! ( "Failed to initialize selected camera '{label}': {err}" ) ,
590+ } )
591+ }
592+
593+ async fn lock_initialized_camera (
594+ camera_feed : & kameo:: actor:: ActorRef < camera:: CameraFeed > ,
595+ id : & camera:: DeviceOrModelID ,
596+ ) -> anyhow:: Result < CameraFeedLock > {
597+ let label = camera_id_label ( id) ;
598+ match camera_feed. ask ( camera:: Lock ) . await {
599+ Ok ( lock) => Ok ( lock) ,
600+ Err ( kameo:: error:: SendError :: HandlerError (
601+ camera:: LockFeedError :: NoInput ,
602+ ) ) => Err ( anyhow ! (
603+ "Selected camera '{label}' did not become ready after initialization"
604+ ) ) ,
605+ Err ( err) => Err ( anyhow ! ( "Failed to lock selected camera '{label}': {err}" ) ) ,
606+ }
607+ }
608+
609+ async fn validate_camera_receiving (
610+ lock : & CameraFeedLock ,
611+ id : & camera:: DeviceOrModelID ,
612+ ) -> anyhow:: Result < ( ) > {
613+ let label = camera_id_label ( id) ;
614+ let ( tx, rx) = flume:: bounded ( 1 ) ;
615+ let remove_sender = tx. clone ( ) ;
616+
617+ lock. ask ( camera:: AddSender ( tx) )
618+ . await
619+ . map_err ( |err| anyhow ! ( "Failed to probe selected camera '{label}': {err}" ) ) ?;
620+
621+ let result = tokio:: time:: timeout ( CAMERA_INPUT_PROBE_TIMEOUT , rx. recv_async ( ) ) . await ;
622+ let _ = lock. ask ( camera:: RemoveSender ( remove_sender) ) . await ;
623+
624+ match result {
625+ Ok ( Ok ( _) ) => Ok ( ( ) ) ,
626+ Ok ( Err ( _) ) => Err ( anyhow ! (
627+ "Selected camera '{label}' stopped before sending a frame"
628+ ) ) ,
629+ Err ( _) => Err ( anyhow ! (
630+ "Selected camera '{label}' is not sending video frames"
631+ ) ) ,
632+ }
633+ }
634+
635+ async fn lock_selected_camera (
636+ camera_feed : & kameo:: actor:: ActorRef < camera:: CameraFeed > ,
637+ selected_id : Option < camera:: DeviceOrModelID > ,
638+ selected_settings : Option < camera:: CameraDeviceSettings > ,
639+ capture_target : & ScreenCaptureTarget ,
640+ ) -> anyhow:: Result < Option < Arc < CameraFeedLock > > > {
641+ let Some ( id) = selected_id else {
642+ if matches ! ( capture_target, ScreenCaptureTarget :: CameraOnly ) {
643+ return Err ( anyhow ! (
644+ "Camera-only recording requires a selected camera. Please select a camera before starting."
645+ ) ) ;
646+ }
647+
648+ return Ok ( None ) ;
649+ } ;
650+
651+ let existing_lock = match camera_feed. ask ( camera:: Lock ) . await {
652+ Ok ( lock) if camera_lock_matches_id ( & lock, & id) => Some ( lock) ,
653+ Ok ( lock) => {
654+ drop ( lock) ;
655+ tokio:: time:: sleep ( Duration :: from_millis ( 50 ) ) . await ;
656+ None
657+ }
658+ Err ( kameo:: error:: SendError :: HandlerError (
659+ camera:: LockFeedError :: NoInput ,
660+ ) ) => None ,
661+ Err ( err) => {
662+ return Err ( anyhow ! (
663+ "Failed to lock selected camera '{}': {err}" ,
664+ camera_id_label( & id)
665+ ) ) ;
666+ }
667+ } ;
668+
669+ let lock = if let Some ( lock) = existing_lock {
670+ lock
671+ } else {
672+ initialize_selected_camera ( camera_feed, & id, selected_settings) . await ?;
673+ lock_initialized_camera ( camera_feed, & id) . await ?
674+ } ;
675+
676+ validate_camera_receiving ( & lock, & id) . await ?;
677+ Ok ( Some ( Arc :: new ( lock) ) )
678+ }
679+
680+ async fn initialize_selected_microphone (
681+ mic_feed : & kameo:: actor:: ActorRef < microphone:: MicrophoneFeed > ,
682+ label : & str ,
683+ settings : Option < microphone:: MicrophoneDeviceSettings > ,
684+ ) -> anyhow:: Result < ( ) > {
685+ let ready = mic_feed
686+ . ask ( microphone:: SetInput {
687+ label : label. to_string ( ) ,
688+ settings,
689+ } )
690+ . await
691+ . map_err ( |err| anyhow ! ( "Failed to initialize selected microphone '{label}': {err}" ) ) ?;
692+
693+ ready. await . map_err ( |err| match err {
694+ microphone:: SetInputError :: DeviceNotFound => {
695+ anyhow ! ( "Selected microphone '{label}' is no longer available" )
696+ }
697+ err => anyhow ! ( "Failed to initialize selected microphone '{label}': {err}" ) ,
698+ } )
699+ }
700+
701+ async fn lock_initialized_microphone (
702+ mic_feed : & kameo:: actor:: ActorRef < microphone:: MicrophoneFeed > ,
703+ label : & str ,
704+ ) -> anyhow:: Result < microphone:: MicrophoneFeedLock > {
705+ match mic_feed. ask ( microphone:: Lock ) . await {
706+ Ok ( lock) => Ok ( lock) ,
707+ Err ( kameo:: error:: SendError :: HandlerError (
708+ microphone:: LockFeedError :: NoInput ,
709+ ) ) => Err ( anyhow ! (
710+ "Selected microphone '{label}' did not become ready after initialization"
711+ ) ) ,
712+ Err ( err) => Err ( anyhow ! ( "Failed to lock selected microphone '{label}': {err}" ) ) ,
713+ }
714+ }
715+
716+ async fn validate_microphone_receiving (
717+ lock : & microphone:: MicrophoneFeedLock ,
718+ label : & str ,
719+ ) -> anyhow:: Result < ( ) > {
720+ let ( tx, rx) = flume:: bounded ( 1 ) ;
721+ let remove_sender = tx. clone ( ) ;
722+
723+ lock. ask ( microphone:: AddSender ( tx) )
724+ . await
725+ . map_err ( |err| anyhow ! ( "Failed to probe selected microphone '{label}': {err}" ) ) ?;
726+
727+ let result = tokio:: time:: timeout ( MICROPHONE_INPUT_PROBE_TIMEOUT , rx. recv_async ( ) ) . await ;
728+ let _ = lock. ask ( microphone:: RemoveSender ( remove_sender) ) . await ;
729+
730+ match result {
731+ Ok ( Ok ( _) ) => Ok ( ( ) ) ,
732+ Ok ( Err ( _) ) => Err ( anyhow ! (
733+ "Selected microphone '{label}' stopped before sending audio"
734+ ) ) ,
735+ Err ( _) => Err ( anyhow ! (
736+ "Selected microphone '{label}' is not sending audio"
737+ ) ) ,
738+ }
739+ }
740+
550741pub fn format_project_name < ' a > (
551742 template : Option < & str > ,
552743 target_name : & ' a str ,
0 commit comments