From 25e979d30ea3928039195a210f7b81f6898b4141 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Thu, 16 May 2024 19:59:30 +0300 Subject: [PATCH 1/5] feat(player): add Seek player action Signed-off-by: Lachezar Lechev --- src/models/player.rs | 154 ++++++++++++++++++++++++++------------ src/runtime/msg/action.rs | 7 ++ 2 files changed, 115 insertions(+), 46 deletions(-) diff --git a/src/models/player.rs b/src/models/player.rs index 09e4571ac..873ed80ce 100644 --- a/src/models/player.rs +++ b/src/models/player.rs @@ -380,6 +380,70 @@ impl UpdateWithCtx for Player { })) .unchanged() } + Msg::Action(Action::Player(ActionPlayer::Seek { + time, + duration, + device, + })) => match (&self.selected, &mut self.library_item) { + ( + // make sure we have a Selected + Some(_selected), + Some(library_item), + ) => { + // We might want to consider whether we want to update the LibraryItem for next video + // like we do for TimeChanged + + // update the last_watched + library_item.state.last_watched = Some(E::now()); + // seek logging + if library_item.r#type == "series" && time < &PLAYER_IGNORE_SEEK_AFTER { + self.seek_history.push(SeekLog { + from: library_item.state.time_offset, + to: *time, + }); + } + // }; + time.clone_into(&mut library_item.state.time_offset); + duration.clone_into(&mut library_item.state.duration); + // No need to check and flag the library item as watched, + // seeking does not update the time_watched! + + // Nor there's a need to update removed and temp, this can only happen + // after we mark a LibraryItem as watched! Leave this to TimeChanged + + // Update the analytics, we still want to keep the correct time and duration updated + if let Some(analytics_context) = &mut self.analytics_context { + library_item + .state + .video_id + .clone_into(&mut analytics_context.video_id); + analytics_context.time = Some(library_item.state.time_offset); + analytics_context.duration = Some(library_item.state.duration); + analytics_context.device_type = Some(device.to_owned()); + analytics_context.device_name = Some(device.to_owned()); + analytics_context.player_duration = Some(duration.to_owned()); + }; + + // on seeking we want to make sure we send the correct Trakt events + let trakt_event_effects = match (self.loaded, self.paused) { + (true, Some(true)) => Effects::msg(Msg::Event(Event::TraktPaused { + context: self.analytics_context.as_ref().cloned().unwrap_or_default(), + })) + .unchanged(), + (true, Some(false)) => Effects::msg(Msg::Event(Event::TraktPlaying { + context: self.analytics_context.as_ref().cloned().unwrap_or_default(), + })) + .unchanged(), + _ => Effects::none(), + }; + + let push_to_library_effects = + push_to_library::(&mut self.push_library_item_time, library_item); + + trakt_event_effects.join(push_to_library_effects) + } + _ => Effects::none().unchanged(), + }, Msg::Action(Action::Player(ActionPlayer::TimeChanged { time, duration, @@ -396,8 +460,6 @@ impl UpdateWithCtx for Player { }), Some(library_item), ) => { - let seeking = library_item.state.time_offset.abs_diff(*time) > 1000; - // if we've selected a new video (like the next episode) library_item.state.last_watched = Some(E::now()); if library_item.state.video_id != Some(video_id.to_owned()) { @@ -409,20 +471,7 @@ impl UpdateWithCtx for Player { library_item.state.time_watched = 0; library_item.state.flagged_watched = 0; } else { - // else we have added to the currently selected video/stream - // seek logging - if seeking - && library_item.r#type == "series" - && time < &PLAYER_IGNORE_SEEK_AFTER - { - self.seek_history.push(SeekLog { - from: library_item.state.time_offset, - to: *time, - }); - } - - let time_watched = - 1000.min(time.saturating_sub(library_item.state.time_offset)); + let time_watched = time.saturating_sub(library_item.state.time_offset); library_item.state.time_watched = library_item.state.time_watched.saturating_add(time_watched); library_item.state.overall_time_watched = library_item @@ -444,13 +493,16 @@ impl UpdateWithCtx for Player { watched_bit_field.set_video(video_id, true); library_item.state.watched = Some(watched_bit_field.into()); } - }; + } + if library_item.temp && library_item.state.times_watched == 0 { library_item.removed = true; - }; + } + if library_item.removed { library_item.temp = true; - }; + } + if let Some(analytics_context) = &mut self.analytics_context { library_item .state @@ -462,34 +514,8 @@ impl UpdateWithCtx for Player { analytics_context.device_name = Some(device.to_owned()); analytics_context.player_duration = Some(duration.to_owned()); }; - let trakt_event_effects = if seeking && self.loaded && self.paused.is_some() { - if self.paused.expect("paused is None") { - Effects::msg(Msg::Event(Event::TraktPaused { - context: self - .analytics_context - .as_ref() - .cloned() - .unwrap_or_default(), - })) - .unchanged() - } else { - Effects::msg(Msg::Event(Event::TraktPlaying { - context: self - .analytics_context - .as_ref() - .cloned() - .unwrap_or_default(), - })) - .unchanged() - } - } else { - Effects::none() - }; - - let push_to_library_effects = - push_to_library::(&mut self.push_library_item_time, library_item); - trakt_event_effects.join(push_to_library_effects) + push_to_library::(&mut self.push_library_item_time, library_item) } _ => Effects::none().unchanged(), }, @@ -529,6 +555,42 @@ impl UpdateWithCtx for Player { }; trakt_event_effects.join(update_library_item_effects) } + Msg::Action(Action::Player(ActionPlayer::PausedChanged { paused })) + if self.selected.is_some() => + { + self.paused = Some(*paused); + let trakt_event_effects = if !self.loaded { + self.loaded = true; + Effects::msg(Msg::Event(Event::PlayerPlaying { + load_time: self + .load_time + .map(|load_time| { + E::now().timestamp_millis() - load_time.timestamp_millis() + }) + .unwrap_or(-1), + context: self.analytics_context.as_ref().cloned().unwrap_or_default(), + })) + .unchanged() + } else if *paused { + Effects::msg(Msg::Event(Event::TraktPaused { + context: self.analytics_context.as_ref().cloned().unwrap_or_default(), + })) + .unchanged() + } else { + Effects::msg(Msg::Event(Event::TraktPlaying { + context: self.analytics_context.as_ref().cloned().unwrap_or_default(), + })) + .unchanged() + }; + let update_library_item_effects = match &self.library_item { + Some(library_item) => Effects::msg(Msg::Internal(Internal::UpdateLibraryItem( + library_item.to_owned(), + ))) + .unchanged(), + _ => Effects::none().unchanged(), + }; + trakt_event_effects.join(update_library_item_effects) + } Msg::Action(Action::Player(ActionPlayer::NextVideo)) => { let seek_history_effects = seek_update::( self.selected.as_ref(), diff --git a/src/runtime/msg/action.rs b/src/runtime/msg/action.rs index f2d580bc3..23d0a0fdf 100644 --- a/src/runtime/msg/action.rs +++ b/src/runtime/msg/action.rs @@ -151,6 +151,13 @@ pub enum ActionPlayer { StreamStateChanged { state: StreamItemState, }, + /// Seek performed by the user when using the seekbar or + /// the shortcuts for seeking + Seek { + time: u64, + duration: u64, + device: String, + }, TimeChanged { time: u64, duration: u64, From e0353cb645be97e23cbe88211c4d21845e8f28c6 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Tue, 28 May 2024 12:48:16 +0300 Subject: [PATCH 2/5] feat(Player): TimeChanged guard against backward moving time Signed-off-by: Lachezar Lechev --- src/models/player.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/models/player.rs b/src/models/player.rs index 873ed80ce..cbb30a6b4 100644 --- a/src/models/player.rs +++ b/src/models/player.rs @@ -479,8 +479,18 @@ impl UpdateWithCtx for Player { .overall_time_watched .saturating_add(time_watched); }; - time.clone_into(&mut library_item.state.time_offset); - duration.clone_into(&mut library_item.state.duration); + + // if we seek forward, time will be < time_offset + // this is the only thing we can guard against! + // + // for both backward and forward seeking we expect the apps to + // send the right actions and update the times accordingly + // when the state changes (from seeking to playing and vice versa) + if time > &library_item.state.time_offset { + time.clone_into(&mut library_item.state.time_offset); + duration.clone_into(&mut library_item.state.duration); + } + if library_item.state.flagged_watched == 0 && library_item.state.time_watched as f64 > library_item.state.duration as f64 * WATCHED_THRESHOLD_COEF From 9f1ecdbd554d2a21eea01c8a0f6f97fafc6330b3 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Tue, 28 May 2024 12:48:37 +0300 Subject: [PATCH 3/5] docs(PlayerAction): Document Seek & TimeChanged actions Signed-off-by: Lachezar Lechev --- src/runtime/msg/action.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/runtime/msg/action.rs b/src/runtime/msg/action.rs index 23d0a0fdf..564630251 100644 --- a/src/runtime/msg/action.rs +++ b/src/runtime/msg/action.rs @@ -152,12 +152,22 @@ pub enum ActionPlayer { state: StreamItemState, }, /// Seek performed by the user when using the seekbar or - /// the shortcuts for seeking + /// the shortcuts for seeking. + /// + /// When transitioning from Seek to TimeChanged and vice-versa + /// we need to make sure to update the other accordingly + /// if we have any type of throttling of these events, + /// otherwise we will get wrong `LibraryItem.state.time_offset`! Seek { time: u64, duration: u64, device: String, }, + /// A normal playback by the video player + /// + /// The time from one TimeChanged action to another can only grow (move forward) + /// and should never go backwards, except when a [`ActionPlayer::Seek`] happen + /// and moves the time backwards. TimeChanged { time: u64, duration: u64, From 82d2a3c6ba6b9d22869e544c4061135d37e49be3 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Wed, 3 Jul 2024 10:32:35 +0300 Subject: [PATCH 4/5] feat(player): flag to enable or disable seek logs collection Signed-off-by: Lachezar Lechev --- src/models/player.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/models/player.rs b/src/models/player.rs index cbb30a6b4..3d8845d7a 100644 --- a/src/models/player.rs +++ b/src/models/player.rs @@ -120,6 +120,11 @@ pub struct Player { pub seek_history: Vec, #[serde(skip_serializing)] pub skip_gaps: Option<(SkipGapsRequest, Loadable)>, + /// Enable or disable Seek log collection. + /// + /// Default: `false` (Do not collect) + #[serde(default, skip_serializing)] + pub collect_seek_logs: bool, } impl UpdateWithCtx for Player { @@ -395,12 +400,15 @@ impl UpdateWithCtx for Player { // update the last_watched library_item.state.last_watched = Some(E::now()); - // seek logging - if library_item.r#type == "series" && time < &PLAYER_IGNORE_SEEK_AFTER { - self.seek_history.push(SeekLog { - from: library_item.state.time_offset, - to: *time, - }); + + if self.collect_seek_logs { + // collect seek history + if library_item.r#type == "series" && time < &PLAYER_IGNORE_SEEK_AFTER { + self.seek_history.push(SeekLog { + from: library_item.state.time_offset, + to: *time, + }); + } } // }; time.clone_into(&mut library_item.state.time_offset); From 324c3174962eab5507a908dda614bb0f5b08da8f Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Mon, 18 Nov 2024 16:35:10 +0200 Subject: [PATCH 5/5] fix(stremio-core-web): WebModel - Player - enable logs Signed-off-by: Lachezar Lechev --- stremio-core-web/src/model/model.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/stremio-core-web/src/model/model.rs b/stremio-core-web/src/model/model.rs index 1a342cc3d..ac66c1444 100644 --- a/stremio-core-web/src/model/model.rs +++ b/stremio-core-web/src/model/model.rs @@ -105,7 +105,10 @@ impl WebModel { installed_addons, addon_details: Default::default(), streaming_server, - player: Default::default(), + player: Player { + collect_seek_logs: true, + ..Default::default() + }, }; ( model,