@@ -4,10 +4,11 @@ use flume::{Receiver, Sender};
44use libmpv2:: { events:: Event , Format , Mpv , SetData } ;
55use native_windows_gui:: { self as nwg, PartialUi } ;
66use std:: {
7+ collections:: VecDeque ,
78 mem, ptr,
89 sync:: {
910 atomic:: { AtomicBool , Ordering } ,
10- Arc ,
11+ Arc , Mutex ,
1112 } ,
1213 thread:: { self , JoinHandle } ,
1314 time:: Duration ,
@@ -39,6 +40,61 @@ struct ObserveProperty {
3940 format : Format ,
4041}
4142
43+ #[ derive( Debug , Default ) ]
44+ struct VideoReadyState {
45+ next_load_id : u64 ,
46+ current_load_id : Option < u64 > ,
47+ pending_load_ids : VecDeque < u64 > ,
48+ active_load_id : Option < u64 > ,
49+ file_loaded_id : Option < u64 > ,
50+ ready_sent_id : Option < u64 > ,
51+ }
52+
53+ impl VideoReadyState {
54+ fn begin_transition ( & mut self , loads_file : bool ) -> u64 {
55+ self . next_load_id += 1 ;
56+ self . current_load_id = Some ( self . next_load_id ) ;
57+ self . file_loaded_id = None ;
58+ self . ready_sent_id = None ;
59+ if loads_file {
60+ self . pending_load_ids . push_back ( self . next_load_id ) ;
61+ }
62+ self . next_load_id
63+ }
64+
65+ fn start_file ( & mut self ) {
66+ self . active_load_id = self . pending_load_ids . pop_front ( ) ;
67+ self . file_loaded_id = None ;
68+ }
69+
70+ fn file_loaded ( & mut self ) {
71+ if self . active_load_id == self . current_load_id {
72+ self . file_loaded_id = self . active_load_id ;
73+ }
74+ }
75+
76+ fn playback_restarted ( & mut self ) -> Option < u64 > {
77+ let load_id = self . current_load_id ?;
78+ if self . file_loaded_id != Some ( load_id) || self . ready_sent_id == Some ( load_id) {
79+ return None ;
80+ }
81+ self . ready_sent_id = Some ( load_id) ;
82+ Some ( load_id)
83+ }
84+ }
85+
86+ type SharedVideoReadyState = Arc < Mutex < VideoReadyState > > ;
87+
88+ fn video_ready_response ( load_id : u64 , ready : bool ) -> String {
89+ RPCResponse :: response_message ( PlayerResponse :: video_ready ( load_id, ready) . to_value ( ) )
90+ }
91+
92+ fn send_video_ready ( rpc_response_sender : & Sender < String > , load_id : u64 , ready : bool ) {
93+ if let Err ( error) = rpc_response_sender. send ( video_ready_response ( load_id, ready) ) {
94+ eprintln ! ( "failed to send video readiness: {error}" ) ;
95+ }
96+ }
97+
4298#[ link( name = "user32" ) ]
4399extern "system" {
44100 fn GetDisplayConfigBufferSizes (
@@ -127,6 +183,7 @@ impl PartialUi for Player {
127183 let ( rpc_response_sender, rpc_response_receiver) = flume:: unbounded ( ) ;
128184 let ( observe_property_sender, observe_property_receiver) = flume:: unbounded ( ) ;
129185 data. channel = ipc:: Channel :: new ( Some ( ( in_msg_sender, rpc_response_receiver) ) ) ;
186+ let video_ready_state = Arc :: new ( Mutex :: new ( VideoReadyState :: default ( ) ) ) ;
130187
131188 let mpv = Arc :: new ( create_mpv ( window_handle) ) ;
132189 let mpv_event_client = mpv
@@ -136,7 +193,8 @@ impl PartialUi for Player {
136193 let _event_thread = create_event_thread (
137194 mpv_event_client,
138195 observe_property_receiver,
139- rpc_response_sender,
196+ rpc_response_sender. clone ( ) ,
197+ Arc :: clone ( & video_ready_state) ,
140198 ) ;
141199 let gpu_video_processing = Arc :: new ( AtomicBool :: new ( false ) ) ;
142200 let _display_thread = create_display_output_thread (
@@ -150,6 +208,8 @@ impl PartialUi for Player {
150208 gpu_video_processing,
151209 observe_property_sender,
152210 in_msg_receiver,
211+ rpc_response_sender,
212+ video_ready_state,
153213 ) ;
154214 // @TODO implement a mechanism to stop threads on `Player` drop if needed
155215
@@ -419,6 +479,7 @@ fn create_event_thread(
419479 mpv_event_client : Mpv ,
420480 observe_property_receiver : Receiver < ObserveProperty > ,
421481 rpc_response_sender : Sender < String > ,
482+ video_ready_state : SharedVideoReadyState ,
422483) -> JoinHandle < ( ) > {
423484 thread:: spawn ( move || {
424485 mpv_event_client
@@ -444,6 +505,33 @@ fn create_event_thread(
444505 None => continue ,
445506 } ;
446507
508+ if matches ! ( & event, Event :: StartFile ) {
509+ video_ready_state
510+ . lock ( )
511+ . expect ( "cannot lock video readiness state" )
512+ . start_file ( ) ;
513+ continue ;
514+ }
515+
516+ if matches ! ( & event, Event :: FileLoaded ) {
517+ video_ready_state
518+ . lock ( )
519+ . expect ( "cannot lock video readiness state" )
520+ . file_loaded ( ) ;
521+ continue ;
522+ }
523+
524+ if matches ! ( & event, Event :: PlaybackRestart ) {
525+ let load_id = video_ready_state
526+ . lock ( )
527+ . expect ( "cannot lock video readiness state" )
528+ . playback_restarted ( ) ;
529+ if let Some ( load_id) = load_id {
530+ send_video_ready ( & rpc_response_sender, load_id, true ) ;
531+ }
532+ continue ;
533+ }
534+
447535 // even if you don't do anything with the events, it is still necessary to empty the event loop
448536 let player_response = match event {
449537 Event :: PropertyChange { name, change, .. } => PlayerResponse (
@@ -479,6 +567,8 @@ fn create_message_thread(
479567 gpu_video_processing : Arc < AtomicBool > ,
480568 observe_property_sender : Sender < ObserveProperty > ,
481569 in_msg_receiver : Receiver < String > ,
570+ rpc_response_sender : Sender < String > ,
571+ video_ready_state : SharedVideoReadyState ,
482572) -> JoinHandle < ( ) > {
483573 thread:: spawn ( move || {
484574 // -- Helpers --
@@ -492,6 +582,13 @@ fn create_message_thread(
492582 let send_command = |cmd : CmdVal | {
493583 let parts: Vec < String > = cmd. into ( ) ;
494584 if let Some ( ( name, args) ) = parts. split_first ( ) {
585+ if name == "loadfile" || name == "stop" {
586+ let load_id = video_ready_state
587+ . lock ( )
588+ . expect ( "cannot lock video readiness state" )
589+ . begin_transition ( name == "loadfile" ) ;
590+ send_video_ready ( & rpc_response_sender, load_id, false ) ;
591+ }
495592 let args = args. iter ( ) . map ( String :: as_str) . collect :: < Vec < _ > > ( ) ;
496593 if let Err ( error) = mpv. command ( name, & args) {
497594 eprintln ! ( "failed to execute MPV command: '{error:#}'" )
@@ -570,3 +667,42 @@ fn create_message_thread(
570667 }
571668 } )
572669}
670+
671+ #[ cfg( test) ]
672+ mod tests {
673+ use super :: VideoReadyState ;
674+
675+ #[ test]
676+ fn ready_follows_file_loaded_restart_once ( ) {
677+ let mut state = VideoReadyState :: default ( ) ;
678+ let load_id = state. begin_transition ( true ) ;
679+ state. start_file ( ) ;
680+ assert_eq ! ( state. playback_restarted( ) , None ) ;
681+ state. file_loaded ( ) ;
682+ assert_eq ! ( state. playback_restarted( ) , Some ( load_id) ) ;
683+ assert_eq ! ( state. playback_restarted( ) , None ) ;
684+ }
685+
686+ #[ test]
687+ fn new_transition_ignores_previous_file_events ( ) {
688+ let mut state = VideoReadyState :: default ( ) ;
689+ state. begin_transition ( true ) ;
690+ state. start_file ( ) ;
691+
692+ let current_load = state. begin_transition ( true ) ;
693+ state. file_loaded ( ) ;
694+ assert_eq ! ( state. playback_restarted( ) , None ) ;
695+
696+ state. start_file ( ) ;
697+ state. file_loaded ( ) ;
698+ assert_eq ! ( state. playback_restarted( ) , Some ( current_load) ) ;
699+ }
700+
701+ #[ test]
702+ fn stop_never_becomes_ready ( ) {
703+ let mut state = VideoReadyState :: default ( ) ;
704+ state. begin_transition ( false ) ;
705+ state. file_loaded ( ) ;
706+ assert_eq ! ( state. playback_restarted( ) , None ) ;
707+ }
708+ }
0 commit comments