Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gui: picture-in-picture mode may cause segfault on wayland environment #798

Merged
merged 2 commits into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
use media_loaded_v2 signal
  • Loading branch information
cosven committed Feb 18, 2024
commit bf3dbfce94c7e2d0e2ba067358046fc28f89f518
34 changes: 24 additions & 10 deletions feeluown/gui/uimain/nowplaying_overlay.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,13 @@ def __init__(self, app: 'GuiApp', parent=None):
self.setup_ui()

self.artwork_view.mv_btn.clicked.connect(self.play_mv)
self.ctl_btns.media_btns.toggle_video_btn.clicked.connect(self.enter_video_mode)
self._app.player.video_channel_changed.connect(
self.on_video_channel_changed, aioqueue=True
self.ctl_btns.media_btns.toggle_video_btn.clicked.connect(
self.keep_and_enter_video_mode
)
self._app.player.media_loaded_v2.connect(
self.on_media_loaded, aioqueue=True
)
self._keep_video_mode = False

def setup_ui(self):
self._layout.setContentsMargins(0, 0, 11, 0)
Expand All @@ -76,20 +79,31 @@ def play_mv(self):
self._app.playlist.set_current_model(self._app.playlist.current_song_mv)
self.enter_video_mode()

def keep_and_enter_video_mode(self):
self._keep_video_mode = True
self.enter_video_mode()

def unkeep_and_enter_cover_mode(self):
self._keep_video_mode = False
self.enter_cover_mode()

def enter_video_mode(self):
# FIXME: should call watch_mgr.set_mode
self._app.watch_mgr.exit_pip_mode()
self._app.watch_mgr.exit_fullwindow_mode()
video_widget = self._app.ui.mpv_widget
video_widget.overlay_auto_visible = True
with video_widget.change_parent():
if video_widget.parent() == self.artwork_view:
self.artwork_view.set_body(video_widget)
else:
with video_widget.change_parent():
self.artwork_view.set_body(video_widget)
self.ctl_btns.hide()
self.progress.hide()
video_widget.ctl_bar.clear_adhoc_btns()
exit_btn = video_widget.ctl_bar.add_adhoc_btn('退出视频模式')
fullwindow_btn = video_widget.ctl_bar.add_adhoc_btn('窗口全屏')
exit_btn.clicked.connect(self.enter_cover_mode)
exit_btn.clicked.connect(self.unkeep_and_enter_cover_mode)
fullwindow_btn.clicked.connect(
lambda: self._app.watch_mgr.
enter_fullwindow_mode(go_back=self.enter_video_mode)
Expand All @@ -115,11 +129,11 @@ def showEvent(self, a0) -> None:
def sizeHint(self):
return QSize(500, 400)

def on_video_channel_changed(self, _):
if (
bool(self._app.player.video_channel) is False
and not self._app.ui.mpv_widget.is_changing_parent
):
def on_media_loaded(self, properties):
if bool(properties['video_format']) is True:
if self._keep_video_mode:
self.enter_video_mode()
else:
self.enter_cover_mode()


Expand Down
69 changes: 32 additions & 37 deletions feeluown/gui/watch.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,22 +57,30 @@ def __init__(self, app: 'GuiApp'):
self._pip_container = ResizableFramelessContainer()
self._fullwindow_container = FullWindowContainer(app, parent=app)

#: Is the video widget visible before.
self._is_visible_before_auto_set_to_none = False
self._keep_fullwindow_mode = False
self._keep_pip_mode = False

def initialize(self):
self._initialize_mpv_video_renderring()

self._ui = self._app.ui
self._ui.pc_panel.media_btns.toggle_video_btn.clicked.connect(
lambda: self.set_mode(Mode.fullwindow))
lambda: self.keep_and_set_mode(Mode.fullwindow))
self._app.player.media_changed.connect(self.on_media_changed, aioqueue=True)
self._app.player.video_channel_changed.connect(self.on_video_channel_changed, aioqueue=True) # noqa
self._app.player.media_loaded_v2.connect(self.on_media_loaded, aioqueue=True)

self._pip_container.setMinimumSize(200, 130)
self._pip_container.hide()
self._fullwindow_container.hide()

def keep_and_set_mode(self, mode):
mode = Mode(mode)
if mode is Mode.fullwindow:
self._keep_fullwindow_mode = True
elif mode is Mode.pip:
self._keep_pip_mode = True
self.set_mode(mode)

def set_mode(self, mode):
mode = Mode(mode) # So that user can call set_mode(0/1/2) in REPL.
if mode is Mode.none:
Expand All @@ -86,14 +94,14 @@ def set_mode(self, mode):
# current mode and enter mode
if mode is Mode.fullwindow:
self.exit_pip_mode()
self.enter_fullwindow_mode(go_back=self.exit_fullwindow_mode)
self.enter_fullwindow_mode(go_back=self.unkeep_and_exit_fullwindow_mode)
else:
self.exit_fullwindow_mode()
self.enter_pip_mode()

def enter_fullwindow_mode(self, go_back=None):
video_widget = self._app.ui.mpv_widget
logger.debug("enter video-show fullwindow mode")
video_widget = self._app.ui.mpv_widget
if video_widget.parent() != self._fullwindow_container:
with video_widget.change_parent():
self._fullwindow_container.set_body(video_widget)
Expand All @@ -104,26 +112,23 @@ def enter_fullwindow_mode(self, go_back=None):
video_widget.overlay_auto_visible = True
video_widget.ctl_bar.clear_adhoc_btns()
pip_btn = video_widget.ctl_bar.add_adhoc_btn('画中画')
pip_btn.clicked.connect(lambda: self.set_mode(Mode.pip))
pip_btn.clicked.connect(lambda: self.keep_and_set_mode(Mode.pip))
if go_back is not None:
hide_btn = video_widget.ctl_bar.add_adhoc_btn('最小化')
hide_btn.clicked.connect(go_back)

def unkeep_pip_and_enter_fullwindow_mode(self):
self._keep_pip_mode = False
self.set_mode(Mode.fullwindow)

def unkeep_and_exit_fullwindow_mode(self):
self._keep_fullwindow_mode = False
self.exit_fullwindow_mode()

def exit_fullwindow_mode(self):
self._app.ui.mpv_widget.hide()
self._fullwindow_container.hide()
logger.debug("exit video-show fullwindow mode")

def _is_pip_mode(self):
return self._app.ui.mpv_widget.parent() == self._pip_container

def _is_fullwindow_mode(self):
return (self._app.ui.mpv_widget.parent() == self._app and
self._app.ui.mpv_widget.isVisible())

def _is_none_mode(self):
return not (self._is_pip_mode() or self._is_fullwindow_mode())

def enter_pip_mode(self):
"""enter picture in picture mode"""
logger.debug("enter video-show picture in picture mode")
Expand All @@ -138,13 +143,13 @@ def enter_pip_mode(self):
fullscreen_btn = video_widget.ctl_bar.add_adhoc_btn('全屏')
hide_btn = video_widget.ctl_bar.add_adhoc_btn('退出画中画')
fullscreen_btn.clicked.connect(self.toggle_pip_fullscreen)
hide_btn.clicked.connect(lambda: self.set_mode(Mode.fullwindow))
hide_btn.clicked.connect(self.unkeep_pip_and_enter_fullwindow_mode)
self._pip_container.show()
self._app.ui.mpv_widget.show()
try:
width = int(self._app.player._mpv.width) # type: ignore
height = int(self._app.player._mpv.height) # type: ignore
except ValueError:
except TypeError:
logger.exception('mpv video width/height is not a valid int')
else:
proper_width = max(min(width, 640), 320)
Expand All @@ -169,24 +174,14 @@ def on_media_changed(self, media):
logger.debug('media is changed to none, hide video-show')
self.set_mode(Mode.none)

def on_video_channel_changed(self, _):
if bool(self._app.player.video_channel) is False:
# When the mpv widget is changing it's parent, the video_channel may be
# changed to empty manully (see mpv_widget.change_parent).
if not self._app.ui.mpv_widget.is_changing_parent:
return

# HELP(cosven): Even if player play a valid video, the video_format
# is changed to none first, and then it is changed to the real value.
# So check if the video widget is visible before hide it.
self._is_visible_before_auto_set_to_none = self._app.ui.mpv_widget.isVisible() # noqa
self.set_mode(Mode.none)
def on_media_loaded(self, properties):
if bool(properties['video_format']) is True:
if self._keep_fullwindow_mode:
self.set_mode(Mode.fullwindow)
elif self._keep_pip_mode:
self.set_mode(Mode.pip)
else:
if self._is_visible_before_auto_set_to_none is True:
if self._is_fullwindow_mode():
self.set_mode(Mode.fullwindow)
elif self._is_pip_mode():
self.set_mode(Mode.pip)
self.set_mode(Mode.none)

#
# private methods
Expand Down
9 changes: 0 additions & 9 deletions feeluown/gui/widgets/mpv_.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ def __init__(self, app, parent=None):
self.mpv = self._app.player._mpv # noqa
self.ctx = None
self.get_proc_addr_c = OpenGlCbGetProcAddrFn(get_proc_addr)
self._is_changing_parent = False

def initializeGL(self):
params = {'get_proc_address': self.get_proc_addr_c}
Expand Down Expand Up @@ -99,26 +98,18 @@ def sizeHint(self):
return QSize(self.mpv.width, self.mpv.height)
return super().sizeHint()

@property
def is_changing_parent(self):
return self._is_changing_parent

@contextmanager
def change_parent(self):
assert self._is_changing_parent is False, 'implementation bug'

# on macOS, changing mpv widget parent cause no side effects.
# on Linux (wayland), it seems changing mpv widget parent may cause segfault,
# so do some hack to avoid crash.
if not IS_MACOS:
self._is_changing_parent = True
self._before_change_mpv_widget_parent()
try:
yield
finally:
if not IS_MACOS:
self._after_change_mpv_widget_parent()
self._is_changing_parent = False

def _before_change_mpv_widget_parent(self):
"""
Expand Down
4 changes: 3 additions & 1 deletion feeluown/player/base_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ def __init__(self, _=None, **kwargs):
self._current_media = None
self._current_metadata = Metadata()
self._video_format = None
self._video_channel = False # False, True, 1, int

#: player position changed signal
self.position_changed = Signal()
Expand All @@ -62,7 +61,10 @@ def __init__(self, _=None, **kwargs):
#: media about to change: (old_media, media)
self.media_about_to_changed = Signal()
self.media_changed = Signal() # Media source is changed (not loaded yet).
# The difference between media_loaded and media_loaded_v2 is that
# media_loaded_v2 carries some media properties.
self.media_loaded = Signal() # Start to play the media.
self.media_loaded_v2 = Signal() # emit(properties)
# Metadata is changed, and it may be changed during playing.
self.metadata_changed = Signal()
self.media_finished = Signal() # Finish to play the media.
Expand Down
19 changes: 1 addition & 18 deletions feeluown/player/mpvplayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ def __init__(self, _=None, audio_device=b'auto', winid=None,

#: if video_format changes to None, there is no video available
self.video_format_changed = Signal()
self.video_channel_changed = Signal()

self._mpv.observe_property(
'time-pos',
Expand Down Expand Up @@ -272,20 +271,6 @@ def video_format(self, vformat):
# firstly changed to None, and then changed to the real format.
self.video_format_changed.emit(vformat)

@property
def video_channel(self):
return self._video_channel

@video_channel.setter
def video_channel(self, value):
"""
According to practice:
- when playing a audio, the video channel is changed to False.
- when playing a video, the video channel is changed to 1.
"""
self._video_channel = value
self.video_channel_changed.emit(value)

def _stop_mpv(self):
# Remove current media.
self._mpv.play("")
Expand All @@ -304,9 +289,6 @@ def _on_duration_changed(self, duration):
def _on_video_format_changed(self, vformat):
self.video_format = vformat

def _on_video_changed(self, video):
self.video_channel = video

def _on_event(self, event):
event_id = event['event_id']
if event_id == MpvEventID.END_FILE:
Expand All @@ -321,6 +303,7 @@ def _on_event(self, event):
elif event_id == MpvEventID.FILE_LOADED:
# If the media is a live streaming, this event may not be received.
self.media_loaded.emit()
self.media_loaded_v2.emit({'video_format': self._mpv.video_format})
elif event_id == MpvEventID.METADATA_UPDATE:
metadata = dict(self._mpv.metadata or {}) # type: ignore
logger.debug('metadata updated to %s', metadata)
Expand Down
Loading