@@ -4,8 +4,9 @@ use flume::{Receiver, Sender};
44use libmpv2:: { events:: Event , Format , Mpv , SetData } ;
55use native_windows_gui:: { self as nwg, PartialUi } ;
66use std:: {
7- collections:: VecDeque ,
8- mem, ptr,
7+ ffi:: { CStr , CString } ,
8+ mem:: { self , MaybeUninit } ,
9+ ptr,
910 sync:: {
1011 atomic:: { AtomicBool , Ordering } ,
1112 Arc , Mutex ,
@@ -44,32 +45,39 @@ struct ObserveProperty {
4445struct VideoReadyState {
4546 next_load_id : u64 ,
4647 current_load_id : Option < u64 > ,
47- pending_load_ids : VecDeque < u64 > ,
48- active_load_id : Option < u64 > ,
48+ current_playlist_entry_id : Option < i64 > ,
49+ active_playlist_entry_id : Option < i64 > ,
4950 file_loaded_id : Option < u64 > ,
5051 ready_sent_id : Option < u64 > ,
5152}
5253
5354impl VideoReadyState {
54- fn begin_transition ( & mut self , loads_file : bool ) -> u64 {
55+ fn begin_transition ( & mut self ) -> u64 {
5556 self . next_load_id += 1 ;
5657 self . current_load_id = Some ( self . next_load_id ) ;
58+ self . current_playlist_entry_id = None ;
59+ self . active_playlist_entry_id = None ;
5760 self . file_loaded_id = None ;
5861 self . ready_sent_id = None ;
59- if loads_file {
60- self . pending_load_ids . push_back ( self . next_load_id ) ;
61- }
6262 self . next_load_id
6363 }
6464
65- fn start_file ( & mut self ) {
66- self . active_load_id = self . pending_load_ids . pop_front ( ) ;
65+ fn bind_playlist_entry ( & mut self , load_id : u64 , playlist_entry_id : i64 ) {
66+ if self . current_load_id == Some ( load_id) {
67+ self . current_playlist_entry_id = Some ( playlist_entry_id) ;
68+ }
69+ }
70+
71+ fn start_file ( & mut self , playlist_entry_id : i64 ) {
72+ self . active_playlist_entry_id = Some ( playlist_entry_id) ;
6773 self . file_loaded_id = None ;
6874 }
6975
7076 fn file_loaded ( & mut self ) {
71- if self . active_load_id == self . current_load_id {
72- self . file_loaded_id = self . active_load_id ;
77+ if self . current_playlist_entry_id . is_some ( )
78+ && self . active_playlist_entry_id == self . current_playlist_entry_id
79+ {
80+ self . file_loaded_id = self . current_load_id ;
7381 }
7482 }
7583
@@ -189,11 +197,18 @@ impl PartialUi for Player {
189197 let mpv_event_client = mpv
190198 . create_client ( None )
191199 . expect ( "cannot create MPV event client" ) ;
200+ let video_ready_event_client = mpv
201+ . create_client ( Some ( "video-ready" ) )
202+ . expect ( "cannot create MPV video readiness client" ) ;
192203
193204 let _event_thread = create_event_thread (
194205 mpv_event_client,
195206 observe_property_receiver,
196207 rpc_response_sender. clone ( ) ,
208+ ) ;
209+ let _video_ready_event_thread = create_video_ready_event_thread (
210+ video_ready_event_client,
211+ rpc_response_sender. clone ( ) ,
197212 Arc :: clone ( & video_ready_state) ,
198213 ) ;
199214 let gpu_video_processing = Arc :: new ( AtomicBool :: new ( false ) ) ;
@@ -264,6 +279,69 @@ fn create_mpv(window_handle: HWND) -> Mpv {
264279 mpv. expect ( "cannot build MPV" )
265280}
266281
282+ fn playlist_entry_id_from_node ( node : & libmpv2_sys:: mpv_node ) -> Option < i64 > {
283+ if node. format != libmpv2_sys:: mpv_format_MPV_FORMAT_NODE_MAP {
284+ return None ;
285+ }
286+
287+ let list = unsafe { node. u . list . as_ref ( ) ? } ;
288+ if list. num <= 0 || list. keys . is_null ( ) || list. values . is_null ( ) {
289+ return None ;
290+ }
291+
292+ for index in 0 ..list. num as usize {
293+ let key = unsafe { * list. keys . add ( index) } ;
294+ if key. is_null ( ) || unsafe { CStr :: from_ptr ( key) } . to_bytes ( ) != b"playlist_entry_id" {
295+ continue ;
296+ }
297+
298+ let value = unsafe { & * list. values . add ( index) } ;
299+ if value. format == libmpv2_sys:: mpv_format_MPV_FORMAT_INT64 {
300+ return Some ( unsafe { value. u . int64 } ) ;
301+ }
302+ }
303+
304+ None
305+ }
306+
307+ fn mpv_error ( error : i32 ) -> String {
308+ let message = unsafe { libmpv2_sys:: mpv_error_string ( error) } ;
309+ if message. is_null ( ) {
310+ return format ! ( "mpv error {error}" ) ;
311+ }
312+ unsafe { CStr :: from_ptr ( message) }
313+ . to_string_lossy ( )
314+ . into_owned ( )
315+ }
316+
317+ fn command_loadfile ( mpv : & Mpv , args : & [ & str ] ) -> Result < i64 , String > {
318+ let strings = std:: iter:: once ( "loadfile" )
319+ . chain ( args. iter ( ) . copied ( ) )
320+ . map ( CString :: new)
321+ . collect :: < Result < Vec < _ > , _ > > ( )
322+ . map_err ( |_| "loadfile argument contains a null byte" . to_string ( ) ) ?;
323+ let mut command = strings
324+ . iter ( )
325+ . map ( |arg| arg. as_ptr ( ) )
326+ . chain ( std:: iter:: once ( ptr:: null ( ) ) )
327+ . collect :: < Vec < _ > > ( ) ;
328+ let mut result = MaybeUninit :: < libmpv2_sys:: mpv_node > :: zeroed ( ) ;
329+
330+ let status = unsafe {
331+ libmpv2_sys:: mpv_command_ret ( mpv. ctx . as_ptr ( ) , command. as_mut_ptr ( ) , result. as_mut_ptr ( ) )
332+ } ;
333+ if status < 0 {
334+ return Err ( mpv_error ( status) ) ;
335+ }
336+
337+ let mut result = unsafe { result. assume_init ( ) } ;
338+ let playlist_entry_id = playlist_entry_id_from_node ( & result) ;
339+ unsafe {
340+ libmpv2_sys:: mpv_free_node_contents ( & mut result) ;
341+ }
342+ playlist_entry_id. ok_or_else ( || "loadfile did not return playlist_entry_id" . to_string ( ) )
343+ }
344+
267345fn create_display_output_thread (
268346 mpv : Arc < Mpv > ,
269347 window_handle : isize ,
@@ -479,12 +557,20 @@ fn create_event_thread(
479557 mut mpv_event_client : Mpv ,
480558 observe_property_receiver : Receiver < ObserveProperty > ,
481559 rpc_response_sender : Sender < String > ,
482- video_ready_state : SharedVideoReadyState ,
483560) -> JoinHandle < ( ) > {
484561 thread:: spawn ( move || {
485562 mpv_event_client
486563 . disable_deprecated_events ( )
487564 . expect ( "failed to disable deprecated MPV events" ) ;
565+ for event in [
566+ libmpv2_sys:: mpv_event_id_MPV_EVENT_START_FILE,
567+ libmpv2_sys:: mpv_event_id_MPV_EVENT_FILE_LOADED,
568+ libmpv2_sys:: mpv_event_id_MPV_EVENT_PLAYBACK_RESTART,
569+ ] {
570+ mpv_event_client
571+ . disable_event ( event)
572+ . expect ( "failed to disable video readiness event" ) ;
573+ }
488574
489575 // -- Event handler loop --
490576
@@ -505,33 +591,6 @@ fn create_event_thread(
505591 None => continue ,
506592 } ;
507593
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-
535594 // even if you don't do anything with the events, it is still necessary to empty the event loop
536595 let player_response = match event {
537596 Event :: PropertyChange { name, change, .. } => PlayerResponse (
@@ -561,6 +620,65 @@ fn create_event_thread(
561620 } )
562621}
563622
623+ fn create_video_ready_event_thread (
624+ mpv_event_client : Mpv ,
625+ rpc_response_sender : Sender < String > ,
626+ video_ready_state : SharedVideoReadyState ,
627+ ) -> JoinHandle < ( ) > {
628+ thread:: spawn ( move || {
629+ mpv_event_client
630+ . disable_all_events ( )
631+ . expect ( "failed to disable MPV events" ) ;
632+ for event in [
633+ libmpv2_sys:: mpv_event_id_MPV_EVENT_START_FILE,
634+ libmpv2_sys:: mpv_event_id_MPV_EVENT_FILE_LOADED,
635+ libmpv2_sys:: mpv_event_id_MPV_EVENT_PLAYBACK_RESTART,
636+ ] {
637+ mpv_event_client
638+ . enable_event ( event)
639+ . expect ( "failed to enable video readiness event" ) ;
640+ }
641+
642+ loop {
643+ let event = unsafe { libmpv2_sys:: mpv_wait_event ( mpv_event_client. ctx . as_ptr ( ) , 0.1 ) } ;
644+ let Some ( event) = ( unsafe { event. as_ref ( ) } ) else {
645+ continue ;
646+ } ;
647+
648+ match event. event_id {
649+ libmpv2_sys:: mpv_event_id_MPV_EVENT_START_FILE => {
650+ let Some ( start_file) = ( unsafe {
651+ ( event. data as * const libmpv2_sys:: mpv_event_start_file ) . as_ref ( )
652+ } ) else {
653+ continue ;
654+ } ;
655+ video_ready_state
656+ . lock ( )
657+ . expect ( "cannot lock video readiness state" )
658+ . start_file ( start_file. playlist_entry_id ) ;
659+ }
660+ libmpv2_sys:: mpv_event_id_MPV_EVENT_FILE_LOADED => {
661+ video_ready_state
662+ . lock ( )
663+ . expect ( "cannot lock video readiness state" )
664+ . file_loaded ( ) ;
665+ }
666+ libmpv2_sys:: mpv_event_id_MPV_EVENT_PLAYBACK_RESTART => {
667+ let load_id = video_ready_state
668+ . lock ( )
669+ . expect ( "cannot lock video readiness state" )
670+ . playback_restarted ( ) ;
671+ if let Some ( load_id) = load_id {
672+ send_video_ready ( & rpc_response_sender, load_id, true ) ;
673+ }
674+ }
675+ libmpv2_sys:: mpv_event_id_MPV_EVENT_SHUTDOWN => break ,
676+ _ => { }
677+ }
678+ }
679+ } )
680+ }
681+
564682fn create_message_thread (
565683 mpv : Arc < Mpv > ,
566684 window_handle : isize ,
@@ -582,16 +700,32 @@ fn create_message_thread(
582700 let send_command = |cmd : CmdVal | {
583701 let parts: Vec < String > = cmd. into ( ) ;
584702 if let Some ( ( name, args) ) = parts. split_first ( ) {
585- if name == "loadfile" || name == "stop" {
703+ let load_id = if name == "loadfile" || name == "stop" {
586704 let load_id = video_ready_state
587705 . lock ( )
588706 . expect ( "cannot lock video readiness state" )
589- . begin_transition ( name == "loadfile" ) ;
707+ . begin_transition ( ) ;
590708 send_video_ready ( & rpc_response_sender, load_id, false ) ;
591- }
709+ Some ( load_id)
710+ } else {
711+ None
712+ } ;
592713 let args = args. iter ( ) . map ( String :: as_str) . collect :: < Vec < _ > > ( ) ;
593- if let Err ( error) = mpv. command ( name, & args) {
594- eprintln ! ( "failed to execute MPV command: '{error:#}'" )
714+ if name == "loadfile" {
715+ let load_id = load_id. expect ( "loadfile transition has no load ID" ) ;
716+ let mut state = video_ready_state
717+ . lock ( )
718+ . expect ( "cannot lock video readiness state" ) ;
719+ match command_loadfile ( & mpv, & args) {
720+ Ok ( playlist_entry_id) => {
721+ state. bind_playlist_entry ( load_id, playlist_entry_id) ;
722+ }
723+ Err ( error) => {
724+ eprintln ! ( "failed to execute MPV command: '{error}'" ) ;
725+ }
726+ }
727+ } else if let Err ( error) = mpv. command ( name, & args) {
728+ eprintln ! ( "failed to execute MPV command: '{error:#}'" ) ;
595729 }
596730 }
597731 } ;
@@ -675,8 +809,9 @@ mod tests {
675809 #[ test]
676810 fn ready_follows_file_loaded_restart_once ( ) {
677811 let mut state = VideoReadyState :: default ( ) ;
678- let load_id = state. begin_transition ( true ) ;
679- state. start_file ( ) ;
812+ let load_id = state. begin_transition ( ) ;
813+ state. bind_playlist_entry ( load_id, 10 ) ;
814+ state. start_file ( 10 ) ;
680815 assert_eq ! ( state. playback_restarted( ) , None ) ;
681816 state. file_loaded ( ) ;
682817 assert_eq ! ( state. playback_restarted( ) , Some ( load_id) ) ;
@@ -686,22 +821,54 @@ mod tests {
686821 #[ test]
687822 fn new_transition_ignores_previous_file_events ( ) {
688823 let mut state = VideoReadyState :: default ( ) ;
689- state. begin_transition ( true ) ;
690- state. start_file ( ) ;
824+ let previous_load = state. begin_transition ( ) ;
825+ state. bind_playlist_entry ( previous_load, 10 ) ;
826+ state. start_file ( 10 ) ;
691827
692- let current_load = state. begin_transition ( true ) ;
828+ let current_load = state. begin_transition ( ) ;
829+ state. bind_playlist_entry ( current_load, 20 ) ;
693830 state. file_loaded ( ) ;
694831 assert_eq ! ( state. playback_restarted( ) , None ) ;
695832
696- state. start_file ( ) ;
833+ state. start_file ( 20 ) ;
834+ state. file_loaded ( ) ;
835+ assert_eq ! ( state. playback_restarted( ) , Some ( current_load) ) ;
836+ }
837+
838+ #[ test]
839+ fn back_to_back_loads_ignore_superseded_entry ( ) {
840+ let mut state = VideoReadyState :: default ( ) ;
841+ let previous_load = state. begin_transition ( ) ;
842+ state. bind_playlist_entry ( previous_load, 10 ) ;
843+
844+ let current_load = state. begin_transition ( ) ;
845+ state. bind_playlist_entry ( current_load, 20 ) ;
846+
847+ state. start_file ( 10 ) ;
848+ state. file_loaded ( ) ;
849+ assert_eq ! ( state. playback_restarted( ) , None ) ;
850+
851+ state. start_file ( 20 ) ;
852+ state. file_loaded ( ) ;
853+ assert_eq ! ( state. playback_restarted( ) , Some ( current_load) ) ;
854+ }
855+
856+ #[ test]
857+ fn load_without_start_file_does_not_desynchronize_next_load ( ) {
858+ let mut state = VideoReadyState :: default ( ) ;
859+ state. begin_transition ( ) ;
860+
861+ let current_load = state. begin_transition ( ) ;
862+ state. bind_playlist_entry ( current_load, 20 ) ;
863+ state. start_file ( 20 ) ;
697864 state. file_loaded ( ) ;
698865 assert_eq ! ( state. playback_restarted( ) , Some ( current_load) ) ;
699866 }
700867
701868 #[ test]
702869 fn stop_never_becomes_ready ( ) {
703870 let mut state = VideoReadyState :: default ( ) ;
704- state. begin_transition ( false ) ;
871+ state. begin_transition ( ) ;
705872 state. file_loaded ( ) ;
706873 assert_eq ! ( state. playback_restarted( ) , None ) ;
707874 }
0 commit comments