Skip to content

Commit bac46d6

Browse files
committed
Merge branch 'main' into update/libmpv2-6
2 parents 91a8280 + 9c92800 commit bac46d6

3 files changed

Lines changed: 165 additions & 4 deletions

File tree

src/stremio_app/stremio_player/communication.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,17 +82,31 @@ impl PlayerEnded {
8282
pub struct PlayerError {
8383
pub error: String,
8484
}
85+
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
86+
#[serde(rename_all = "camelCase")]
87+
pub struct PlayerVideoReady {
88+
pub load_id: u64,
89+
pub ready: bool,
90+
}
8591
#[derive(Serialize, Deserialize, Debug, Clone)]
8692
#[serde(untagged)]
8793
pub enum PlayerEvent {
8894
PropChange(PlayerProprChange),
8995
End(PlayerEnded),
9096
Error(PlayerError),
97+
VideoReady(PlayerVideoReady),
9198
}
9299

93100
#[derive(Serialize, Deserialize, Debug, Clone)]
94101
pub struct PlayerResponse<'a>(pub &'a str, pub PlayerEvent);
95102
impl PlayerResponse<'_> {
103+
pub fn video_ready(load_id: u64, ready: bool) -> PlayerResponse<'static> {
104+
PlayerResponse(
105+
"mpv-event-video-ready",
106+
PlayerEvent::VideoReady(PlayerVideoReady { load_id, ready }),
107+
)
108+
}
109+
96110
pub fn to_value(&self) -> Option<serde_json::Value> {
97111
serde_json::to_value(self).ok()
98112
}

src/stremio_app/stremio_player/communication_tests.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
use crate::stremio_app::stremio_player::communication::{
2-
BoolProp, CmdVal, InMsg, InMsgArgs, InMsgFn, MpvCmd, PlayerEnded, PlayerProprChange, PropKey,
3-
PropVal,
2+
BoolProp, CmdVal, InMsg, InMsgArgs, InMsgFn, MpvCmd, PlayerEnded, PlayerProprChange,
3+
PlayerResponse, PropKey, PropVal,
44
};
55
use libmpv2::{events::PropertyData, mpv_end_file_reason};
66

77
use serde_test::{assert_tokens, Token};
88

9+
#[test]
10+
fn video_ready_response() {
11+
assert_eq!(
12+
PlayerResponse::video_ready(7, true).to_value(),
13+
Some(serde_json::json!([
14+
"mpv-event-video-ready",
15+
{ "loadId": 7, "ready": true }
16+
]))
17+
);
18+
}
19+
920
#[test]
1021
fn propr_change_tokens() {
1122
let prop = "test-prop";

src/stremio_app/stremio_player/player.rs

Lines changed: 138 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ 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,
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")]
4399
extern "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

Comments
 (0)