Skip to content

Commit 88d930c

Browse files
committed
Correlate video readiness with mpv loads
1 parent 88a8d71 commit 88d930c

1 file changed

Lines changed: 219 additions & 52 deletions

File tree

src/stremio_app/stremio_player/player.rs

Lines changed: 219 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ use flume::{Receiver, Sender};
44
use libmpv2::{events::Event, Format, Mpv, SetData};
55
use native_windows_gui::{self as nwg, PartialUi};
66
use 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 {
4445
struct 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

5354
impl 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+
267345
fn 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+
564682
fn 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

Comments
 (0)