From 81b551791f8b86f37a1428c3a68121949198e437 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Wed, 13 Mar 2024 18:37:14 +0100 Subject: [PATCH] pause videos by default unless in playlist mode BUG: setting playback position appears to break peertube playback the smae thing also appears to happend with invidious on slow conenctions --- melon/models/__init__.py | 11 +++++ melon/player/__init__.py | 23 ++++++--- melon/player/playlist.py | 7 +++ melon/servers/__init__.py | 16 ++++++ melon/servers/invidious/__init__.py | 49 ++++++++++++++++++ melon/servers/peertube/__init__.py | 77 ++++++++++++++++++++++++----- 6 files changed, 166 insertions(+), 17 deletions(-) diff --git a/melon/models/__init__.py b/melon/models/__init__.py index 1cac642..5ae25bf 100644 --- a/melon/models/__init__.py +++ b/melon/models/__init__.py @@ -725,6 +725,17 @@ def get_last_feed_refresh() -> int: return app_conf["news-feed-refresh"] return None +def get_video_duration(server_id:str, video_id:str) -> (float|None): + conn = connect_to_db() + results = conn.execute(""" + SELECT duration + FROM playback + WHERE video_id = ? AND video_server = ? + """, (video_id, server_id)).fetchone() + if results is None: + return None + return results[0] + def get_video_playback_position(server_id:str, video_id:str) -> (float|None): conn = connect_to_db() results = conn.execute(""" diff --git a/melon/player/__init__.py b/melon/player/__init__.py index c4283d4..ac730de 100644 --- a/melon/player/__init__.py +++ b/melon/player/__init__.py @@ -13,7 +13,7 @@ from melon.widgets.feeditem import AdaptiveFeedItem from melon.widgets.preferencerow import PreferenceRow, PreferenceType, Preference from melon.widgets.iconbutton import IconButton from melon.models import get_app_settings, add_to_history, register_callback -from melon.models import get_video_playback_position, set_video_playback_position, set_video_playback_position_force +from melon.models import get_video_playback_position, set_video_playback_position, set_video_playback_position_force, get_video_duration from melon.utils import pass_me class PlayerScreen(Adw.NavigationPage): @@ -151,7 +151,7 @@ class PlayerScreen(Adw.NavigationPage): # reset playback position if video was nearly completely watched pos = get_video_playback_position(server_id, video_id) - dur = get_video_playback_position(server_id, video_id) + dur = get_video_duration(server_id, video_id) if not pos is None and not dur is None: # TODO consider moving this to the settings panel consider_done = 0.99*dur @@ -219,9 +219,15 @@ class PlayerScreen(Adw.NavigationPage): self.position = pos else: self.position = None - # get video duration & update db - # handled by self.on_duration - self.instance.get_video_duration(self.view, self.on_duration) + self.prepare_player_state() + + def prepare_player_state(self): + self.instance.do_video_pause(self.view, self.prepared_player_state()) + + def prepared_player_state(self): + # get video duration & update db + # handled by self.on_duration + self.instance.get_video_duration(self.view, self.on_duration) def on_duration(self, duration): self.duration = duration @@ -231,6 +237,7 @@ class PlayerScreen(Adw.NavigationPage): else: self.on_player_setup_done() def on_player_setup_done(self): + # save data self.commit_playback() # connect to events self.connect("hiding", lambda _: self.on_hide()) @@ -267,7 +274,11 @@ class PlayerScreen(Adw.NavigationPage): def store_position(self, after=None, commit=True): self.instance.get_video_playback_position(self.view, pass_me(self.on_position, after, commit)) def on_position(self, position, after=None, commit=True): - self.position = position + if not position is None: + # only save valid datasets + self.position = position + # because this often occurs when killing the webview + # or on_hide, we still want to commit the changes if commit: self.commit_playback() if not after is None: diff --git a/melon/player/playlist.py b/melon/player/playlist.py index e245a59..44b98ac 100644 --- a/melon/player/playlist.py +++ b/melon/player/playlist.py @@ -289,3 +289,10 @@ class PlaylistPlayerScreen(PlayerScreen): self.ctr_group.add(expander) self.about.add(self.ctr_group) + + def prepare_player_state(self): + # by default the player pauses the video + # so that the user has to click "play" + # however in playlist mode, we kind of want videos to autoplay + # especially after the first one + self.instance.do_video_play(self.view, self.prepared_player_state()) diff --git a/melon/servers/__init__.py b/melon/servers/__init__.py index b636ec0..a48e26a 100644 --- a/melon/servers/__init__.py +++ b/melon/servers/__init__.py @@ -294,3 +294,19 @@ class Server(ABC): should pass False to onDone if there was an error """ on_done(False) + + + def do_video_play(self, webview: WebKit.WebView, on_done: Callable[[bool], None]): + """ + Should attempt to start playing the video + i.e by calling the .play() method via javascript + if that worked out True should be passed to on_done + """ + on_done(False) + def do_video_pause(self, webview: WebKit.WebView, on_done: Callable[[bool], None]): + """ + Should attempt to stop playing the video + i.e by calling the .pause() method via javascript + if that worked out True should be passed to on_done + """ + on_done(False) diff --git a/melon/servers/invidious/__init__.py b/melon/servers/invidious/__init__.py index 0237ef9..2d797e1 100644 --- a/melon/servers/invidious/__init__.py +++ b/melon/servers/invidious/__init__.py @@ -332,6 +332,20 @@ class Invidious(Server): }); }); } + let readyState = 2; + if (player.readyState < readyState) { + await new Promise(res=>{ + if (player.readyState >= readyState) { res(); return; } + let list = player.addEventListener( + 'loadeddata', + ()=>{ + if (player.readyState >= readyState) { + player.removeEventListener('loadeddata', list); + res(); + } + }); + }); + } """ def get_video_playback_position(self, webview, on_data): @@ -417,6 +431,41 @@ class Invidious(Server): pass_me(self.on_js_bool, webview, on_done) ) + def do_video_play(self, webview, on_done): + js = f""" + let players = document.getElementsByTagName('video'); + if (players.length != 1) return false; + let player = players[0]; + {self.js_player_ready} + await player.play(); + return true; + """ + webview.call_async_javascript_function( + js, len(js), + None, + None, + self.id, + None, + pass_me(self.on_js_bool, webview, on_done) + ) + def do_video_pause(self, webview, on_done): + js = f""" + let players = document.getElementsByTagName('video'); + if (players.length != 1) return false; + let player = players[0]; + {self.js_player_ready} + player.pause(); + return true; + """ + webview.call_async_javascript_function( + js, len(js), + None, + None, + self.id, + None, + pass_me(self.on_js_bool, webview, on_done) + ) + def on_js_bool(self, obj, async_res, webview, on_bool): if on_bool is None: return diff --git a/melon/servers/peertube/__init__.py b/melon/servers/peertube/__init__.py index 8293c88..7a27656 100644 --- a/melon/servers/peertube/__init__.py +++ b/melon/servers/peertube/__init__.py @@ -295,16 +295,22 @@ class Peertube(Server): results = [] if r.ok: for item in r.json()["data"]: - item = item["video"] - video_host = instance - video_slug = item["uuid"] - video_id=f"{video_host}::{video_slug}" - thumb_path = item["thumbnailPath"] - thumb = f"{instance}{thumb_path}" - channel_name = item["channel"]["displayName"] - channel_slug = item["channel"]["name"] - channel_host = item["channel"]["host"] - channel_id = f"https://{channel_host}::{channel_slug}" + try: + item = item["video"] + video_host = instance + video_slug = item["uuid"] + video_id=f"{video_host}::{video_slug}" + thumb_path = item["thumbnailPath"] + thumb = f"{instance}{thumb_path}" + channel_name = item["channel"]["displayName"] + channel_slug = item["channel"]["name"] + channel_host = item["channel"]["host"] + channel_id = f"https://{channel_host}::{channel_slug}" + except Exception as e: + # skip invaldi entries + # most likely a deleted video + # which we can't display + continue v = Video( self.id, item["url"], @@ -389,7 +395,7 @@ class Peertube(Server): js_player_ready = """ if (isNaN(player.duration)) { await new Promise((res)=>{ - if (!isNaN(player.duration)) { res(); return undefined; } + if (!isNaN(player.duration)) { res(); return; } let list = player.addEventListener( 'loadedmetadata', ()=>{ @@ -398,6 +404,20 @@ class Peertube(Server): }); }); } + let readyState = 2; + if (player.readyState < readyState) { + await new Promise(res=>{ + if (player.readyState >= readyState) { res(); return; } + let list = player.addEventListener( + 'loadeddata', + ()=>{ + if (player.readyState >= readyState) { + player.removeEventListener('loadeddata', list); + res(); + } + }); + }); + } """ js_has_player = """ @@ -516,6 +536,41 @@ class Peertube(Server): pass_me(self.on_js_bool, webview, on_done) ) + def do_video_play(self, webview, on_done): + js = f""" + {self.js_has_player} + let players = document.getElementsByTagName('video'); + if (players.length != 1) return false; + let player = players[0]; + await player.play(); + return true; + """ + webview.call_async_javascript_function( + js, len(js), + None, + None, + self.id, + None, + pass_me(self.on_js_bool, webview, on_done) + ) + def do_video_pause(self, webview, on_done): + js = f""" + {self.js_has_player} + let players = document.getElementsByTagName('video'); + if (players.length != 1) return false; + let player = players[0]; + player.pause(); + return true; + """ + webview.call_async_javascript_function( + js, len(js), + None, + None, + self.id, + None, + pass_me(self.on_js_bool, webview, on_done) + ) + def on_js_bool(self, obj, async_res, webview, on_bool): if on_bool is None: return -- 2.38.5