hydrus/hydrus/client/gui/canvas/ClientGUIMPV.py

964 lines
31 KiB
Python
Raw Normal View History

2020-01-16 02:08:23 +00:00
import locale
2020-03-18 21:35:57 +00:00
import os
2023-10-11 20:46:40 +00:00
import time
2020-02-12 22:50:37 +00:00
import traceback
2023-03-29 20:57:59 +00:00
import typing
2020-02-12 22:50:37 +00:00
2020-04-22 21:00:35 +00:00
from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
2023-08-09 21:12:17 +00:00
from hydrus.core import HydrusAnimationHandling
2020-04-22 21:00:35 +00:00
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusPaths
2020-07-29 20:52:44 +00:00
from hydrus.client import ClientApplicationCommand as CAC
2022-01-26 21:57:04 +00:00
from hydrus.client import ClientConstants as CC
2020-04-22 21:00:35 +00:00
from hydrus.client.gui import ClientGUIMedia
from hydrus.client.gui import ClientGUIMediaControls
from hydrus.client.gui import ClientGUIShortcuts
from hydrus.client.gui import QtPorting as QP
2023-06-28 20:29:14 +00:00
from hydrus.client.gui.canvas import ClientGUIMediaVolume
2023-03-29 20:57:59 +00:00
from hydrus.client.media import ClientMedia
2020-04-22 21:00:35 +00:00
2020-02-12 22:50:37 +00:00
mpv_failed_reason = 'MPV seems ok!'
2020-01-16 02:08:23 +00:00
try:
import mpv
MPV_IS_AVAILABLE = True
2020-02-05 22:55:21 +00:00
except Exception as e:
2020-01-16 02:08:23 +00:00
2020-02-12 22:50:37 +00:00
mpv_failed_reason = traceback.format_exc()
2020-01-16 02:08:23 +00:00
MPV_IS_AVAILABLE = False
2023-02-22 21:57:10 +00:00
2020-01-16 02:08:23 +00:00
2023-06-21 19:50:13 +00:00
damaged_file_hashes = set()
2020-01-16 02:08:23 +00:00
def GetClientAPIVersionString():
try:
( major, minor ) = mpv._mpv_client_api_version()
return '{}.{}'.format( major, minor )
except:
return 'unknown'
2020-02-12 22:50:37 +00:00
# issue about mouse-to-osc interactions:
'''
def mouseMoveEvent( self, event ):
# same deal here as with mousereleaseevent--osc is non-interactable with commands, so let's not use it for now
#self._player.command( 'mouse', event.x(), event.y() )
event.ignore()
def mouseReleaseEvent( self, event ):
# left index = 0
# right index = 2
# the issue with using this guy is it sends a mouse press or mouse down event, and the OSC only responds to mouse up
#self._player.command( 'mouse', event.x(), event.y(), index, 'single' )
event.ignore()
'''
2023-06-21 19:50:13 +00:00
def EmergencyDumpOutGlobal( probably_crashy, reason ):
# this is Qt thread so we can talk to this guy no prob
MPVHellBasket.instance().emergencyDumpOut.emit( probably_crashy, reason )
def log_handler( loglevel, component, message ):
2023-06-21 19:50:13 +00:00
# ok important bug dude, if you have multiple mpv windows and hence log handlers, then somehow the mpv dll or python-mpv wrapper is delivering at least some log events to the wrong player's event loop
# so my mapping here to preserve the mpv widget for a particular log message and then dump out the player in emergency is only going to work half the time
nah_it_is_fine_bro_tests = [
'rescan-external-files' in message
]
if True in nah_it_is_fine_bro_tests and not HG.mpv_report_mode:
return
if loglevel == 'error' and 'ffmpeg' in component:
probably_crashy_tests = [
'Invalid NAL unit size' in message,
'Error splitting the input' in message
]
HG.client_controller.CallBlockingToQt( HG.client_controller.gui, EmergencyDumpOutGlobal, True in probably_crashy_tests, f'{component}: {message}' )
HydrusData.DebugPrint( '[MPV {}] {}: {}'.format( loglevel, component, message ) )
2022-09-28 17:15:23 +00:00
2023-10-11 20:46:40 +00:00
MPVShutdownEventType = QP.registerEventType()
class MPVShutdownEvent( QC.QEvent ):
def __init__( self ):
QC.QEvent.__init__( self, MPVShutdownEventType )
2022-09-28 17:15:23 +00:00
MPVFileLoadedEventType = QP.registerEventType()
class MPVFileLoadedEvent( QC.QEvent ):
def __init__( self ):
QC.QEvent.__init__( self, MPVFileLoadedEventType )
2023-06-21 19:50:13 +00:00
'''
MPVLogEventType = QP.registerEventType()
2022-09-28 17:15:23 +00:00
2023-06-21 19:50:13 +00:00
class MPVLogEvent( QC.QEvent ):
def __init__( self, player, event ):
QC.QEvent.__init__( self, MPVLogEventType )
self.player = player
self.event = event
'''
2022-09-28 17:15:23 +00:00
MPVFileSeekedEventType = QP.registerEventType()
class MPVFileSeekedEvent( QC.QEvent ):
def __init__( self ):
QC.QEvent.__init__( self, MPVFileSeekedEventType )
2023-06-21 19:50:13 +00:00
class MPVHellBasket( QC.QObject ):
emergencyDumpOut = QC.Signal( bool, str )
my_instance = None
def __init__( self ):
QC.QObject.__init__( self )
MPVHellBasket.my_instance = self
@staticmethod
def instance() -> 'MPVHellBasket':
if MPVHellBasket.my_instance is None:
MPVHellBasket.my_instance = MPVHellBasket()
return MPVHellBasket.my_instance
2022-09-28 17:15:23 +00:00
LOCALE_IS_SET = False
2020-01-16 02:08:23 +00:00
#Not sure how well this works with hardware acceleration. This just renders to a QWidget. In my tests it seems fine, even with vdpau video out, but I'm not 100% sure it actually uses hardware acceleration.
#Here is an example on how to render into a QOpenGLWidget instead: https://gist.github.com/cosven/b313de2acce1b7e15afda263779c0afc
class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
2020-01-16 02:08:23 +00:00
2020-04-08 21:10:11 +00:00
launchMediaViewer = QC.Signal()
2020-01-16 02:08:23 +00:00
def __init__( self, parent ):
QW.QWidget.__init__( self, parent )
CAC.ApplicationCommandProcessorMixin.__init__( self )
2020-01-16 02:08:23 +00:00
2022-01-26 21:57:04 +00:00
self._canvas_type = CC.CANVAS_PREVIEW
2020-02-05 22:55:21 +00:00
2020-02-26 22:28:52 +00:00
self._stop_for_slideshow = False
2020-02-19 21:48:36 +00:00
2023-10-11 20:46:40 +00:00
# ok, if you talk to this object during an eventPaint while it is in various states of 'null', you'll get this instability problem:
# QBackingStore::endPaint() called with active painter; did you forget to destroy it or call QPainter::end() on it?
# simply calling a do-nothing GetAnimationBarStatus stub that returns immediately will cause this, so it must be some C++ wrapper magic triggering some during-paint reset/event-cycle/whatever
#
# #####
# THUS, DO NOT EVER TALK TO THIS GUY DURING A paintEvent. fetch your data and call update() if it changed. Also, we now make sure _something_ is loaded as much as possible, even if it is a black square png
# #####
#
self._black_png_path = os.path.join( HC.STATIC_DIR, 'blacksquare.png' )
self._hydrus_png_path = os.path.join( HC.STATIC_DIR, 'hydrus.png' )
self._currently_in_media_load_error_state = False
2022-09-28 17:15:23 +00:00
global LOCALE_IS_SET
if not LOCALE_IS_SET:
# This is necessary since PyQT stomps over the locale settings needed by libmpv.
# This needs to happen after importing PyQT before creating the first mpv.MPV instance.
locale.setlocale( locale.LC_NUMERIC, 'C' )
LOCALE_IS_SET = True
2020-01-16 02:08:23 +00:00
self.setAttribute( QC.Qt.WA_DontCreateNativeAncestors )
self.setAttribute( QC.Qt.WA_NativeWindow )
2022-09-28 17:15:23 +00:00
# loglevels: fatal, error, debug
2023-06-21 19:50:13 +00:00
loglevel = 'debug' if HG.mpv_report_mode else 'error'
2023-06-21 19:50:13 +00:00
self._player = mpv.MPV(
wid = str( int( self.winId() ) ),
log_handler = log_handler,
loglevel = loglevel
)
2020-01-16 02:08:23 +00:00
# hydev notes on OSC:
# OSC is by default off, default input bindings are by default off
# difficult to get this to intercept mouse/key events naturally, so you have to pipe them to the window with 'command', but this is not excellent
# general recommendation when using libmpv is to just implement your own stuff anyway, so let's do that for prototype
2022-09-28 17:15:23 +00:00
# self._player[ 'osd-level' ] = 1
# self._player[ 'input-default-bindings' ] = True
2020-01-16 02:08:23 +00:00
2022-11-16 21:34:30 +00:00
self._previous_conf_content_bytes = b''
2020-02-26 22:28:52 +00:00
self.UpdateConf()
2020-01-16 02:08:23 +00:00
self._player.loop = True
2020-01-22 21:04:43 +00:00
# this makes black screen for audio (rather than transparent)
2020-01-16 02:08:23 +00:00
self._player.force_window = True
2020-03-11 21:52:11 +00:00
# this actually propagates up to the OS-level sound mixer lmao, otherwise defaults to ugly hydrus filename
self._player.title = 'hydrus mpv player'
2020-05-20 21:36:02 +00:00
# pass up un-button-pressed mouse moves to parent, which wants to do cursor show/hide
self.setMouseTracking( True )
2020-01-16 02:08:23 +00:00
#self.setFocusPolicy(QC.Qt.StrongFocus)#Needed to get key events
self._player.input_cursor = False#Disable mpv mouse move/click event capture
self._player.input_vo_keyboard = False#Disable mpv key event capture, might also need to set input_x11_keyboard
self._media = None
2023-10-11 20:46:40 +00:00
self._file_header_is_loaded = False
2021-02-11 01:59:52 +00:00
self._disallow_seek_on_this_file = False
2023-06-21 19:50:13 +00:00
self._have_shown_human_error_on_this_file = False
2020-05-27 21:27:52 +00:00
2023-02-08 20:19:41 +00:00
self._times_to_play_animation = 0
2020-02-26 22:28:52 +00:00
2020-02-19 21:48:36 +00:00
self._current_seek_to_start_count = 0
2020-05-20 21:36:02 +00:00
self._InitialiseMPVCallbacks()
2020-01-16 02:08:23 +00:00
self.destroyed.connect( self._player.terminate )
2020-02-05 22:55:21 +00:00
HG.client_controller.sub( self, 'UpdateAudioMute', 'new_audio_mute' )
HG.client_controller.sub( self, 'UpdateAudioVolume', 'new_audio_volume' )
2020-02-26 22:28:52 +00:00
HG.client_controller.sub( self, 'UpdateConf', 'notify_new_options' )
HG.client_controller.sub( self, 'SetLogLevel', 'set_mpv_log_level' )
2020-01-22 21:04:43 +00:00
2022-09-28 17:15:23 +00:00
self.installEventFilter( self )
2020-02-12 22:50:37 +00:00
self._my_shortcut_handler = ClientGUIShortcuts.ShortcutsHandler( self, [], catch_mouse = True )
try:
self.we_are_newer_api = float( GetClientAPIVersionString() ) >= 2.0
except:
self.we_are_newer_api = False
2023-06-21 19:50:13 +00:00
MPVHellBasket.instance().emergencyDumpOut.connect( self.EmergencyDumpOut )
2023-10-11 20:46:40 +00:00
self._player.loadfile( self._black_png_path )
2020-02-05 22:55:21 +00:00
def _GetAudioOptionNames( self ):
2022-01-26 21:57:04 +00:00
if self._canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES:
2020-02-05 22:55:21 +00:00
if HG.client_controller.new_options.GetBoolean( 'media_viewer_uses_its_own_audio_volume' ):
return ClientGUIMediaControls.volume_types_to_option_names[ ClientGUIMediaControls.AUDIO_MEDIA_VIEWER ]
2022-01-26 21:57:04 +00:00
elif self._canvas_type == CC.CANVAS_PREVIEW:
2020-02-05 22:55:21 +00:00
if HG.client_controller.new_options.GetBoolean( 'preview_uses_its_own_audio_volume' ):
return ClientGUIMediaControls.volume_types_to_option_names[ ClientGUIMediaControls.AUDIO_PREVIEW ]
return ClientGUIMediaControls.volume_types_to_option_names[ ClientGUIMediaControls.AUDIO_GLOBAL ]
2023-10-11 20:46:40 +00:00
def _HandleLoadError( self ):
# file failed to load, and we are going to start getting what seem to be C++ level paintEvent exceptions after the GUI object is touched by code and then asked for repaints
self._file_header_is_loaded = False
self._currently_in_media_load_error_state = True
self._player.loadfile( self._hydrus_png_path )
if self._media is not None:
HydrusData.ShowText( f'The file with hash "{self._media.GetHash().hex()}" seems to have failed to load in mpv. In order to preserve program stability, I have unloaded it immediately!' )
2020-05-20 21:36:02 +00:00
def _InitialiseMPVCallbacks( self ):
2022-09-28 17:15:23 +00:00
player = self._player
2023-05-24 20:44:12 +00:00
# note that these happen on the mpv mainloop, not UI code, so we need to post events to stay stable
2022-09-28 17:15:23 +00:00
@player.event_callback( mpv.MpvEventID.SEEK )
def seek_event( event ):
2020-05-27 21:27:52 +00:00
2022-09-28 17:15:23 +00:00
QW.QApplication.instance().postEvent( self, MPVFileSeekedEvent() )
2020-05-27 21:27:52 +00:00
2022-09-28 17:15:23 +00:00
@player.event_callback( mpv.MpvEventID.FILE_LOADED )
def file_loaded_event( event ):
QW.QApplication.instance().postEvent( self, MPVFileLoadedEvent() )
2020-05-27 21:27:52 +00:00
2023-10-11 20:46:40 +00:00
@player.event_callback( mpv.MpvEventID.SHUTDOWN )
def file_started_event( event ):
app = QW.QApplication.instance()
if app is not None and QP.isValid( self ):
app.postEvent( self, MPVShutdownEvent() )
2023-06-21 19:50:13 +00:00
'''
@player.event_callback( mpv.MpvEventID.LOG_MESSAGE )
def log_event( event ):
QW.QApplication.instance().postEvent( self, MPVLogEvent( player, event ) )
'''
2022-09-28 17:15:23 +00:00
2023-10-11 20:46:40 +00:00
def _LooksLikeALoadError( self ):
# as an additional note for the error we are handling here, this isn't catching something like 'error: truncated gubbins', but instead the 'verbose' debug level message of 'ffmpeg can't handle this apng's format, update ffmpeg'
# what happens in this state is the media is unloaded and the self._player.path goes from a valid path to None
# the extra fun is that self._player.path starts as None even after self._player.loadfile and may not be the valid path get as of the LoadedEvent. that event is sent when headers are read, not data
# so we need to detect when the data is actually loaded, after the .path was (briefly) something valid, and then switches back to None
# thankfully, it seems on the dump-out unload, the playlist is unset, and this appears to be the most reliable indicator of a problem and an mpv with nothing currently loaded!
if self._player.path is None:
playlist = self._player.playlist
if len( playlist ) == 0:
return True
for item in playlist:
if 'current' in item:
return False
return True
return False
2022-09-28 17:15:23 +00:00
def ClearMedia( self ):
self.SetMedia( None )
def eventFilter( self, watched, event ):
2023-06-21 19:50:13 +00:00
try:
2020-05-20 21:36:02 +00:00
2023-06-21 19:50:13 +00:00
if event.type() == MPVFileLoadedEventType:
2020-05-27 21:27:52 +00:00
2023-10-11 20:46:40 +00:00
if self._player.path is None:
if self._LooksLikeALoadError():
self._HandleLoadError()
if not self._currently_in_media_load_error_state:
self._file_header_is_loaded = True
2020-05-27 21:27:52 +00:00
2023-06-21 19:50:13 +00:00
return True
2020-05-20 21:36:02 +00:00
2023-06-21 19:50:13 +00:00
elif event.type() == MPVFileSeekedEventType:
2020-05-20 21:36:02 +00:00
2023-10-11 20:46:40 +00:00
if not self._file_header_is_loaded:
2020-05-20 21:36:02 +00:00
2023-06-21 19:50:13 +00:00
return True
2020-05-20 21:36:02 +00:00
2023-06-21 19:50:13 +00:00
current_timestamp_s = self._player.time_pos
if self._media is not None and current_timestamp_s is not None and current_timestamp_s <= 1.0:
2020-05-20 21:36:02 +00:00
2023-06-21 19:50:13 +00:00
self._current_seek_to_start_count += 1
2020-05-20 21:36:02 +00:00
2023-06-21 19:50:13 +00:00
if self._stop_for_slideshow:
self.Pause()
if self._times_to_play_animation != 0 and self._current_seek_to_start_count >= self._times_to_play_animation:
self.Pause()
return True
2020-05-20 21:36:02 +00:00
2023-10-11 20:46:40 +00:00
elif event.type() == MPVShutdownEventType:
self.setVisible( False )
2020-05-20 21:36:02 +00:00
2023-06-21 19:50:13 +00:00
except Exception as e:
HydrusData.ShowException( e )
2022-09-28 17:15:23 +00:00
return True
2020-05-20 21:36:02 +00:00
2022-09-28 17:15:23 +00:00
return False
2021-02-11 01:59:52 +00:00
2023-06-21 19:50:13 +00:00
def EmergencyDumpOut( self, probably_crashy, reason ):
# we had to rewrite this thing due to some threading/loop/event issues at the lower mpv level
# when we have an emergency, we now broadcast to all mpv players at once, they all crash out, to be safe
original_media = self._media
if original_media is None:
# this MPV window is probably not the one that had a problem
return
media_line = '\n\nIts hash is: {}'.format( original_media.GetHash().hex() )
if probably_crashy:
self.ClearMedia()
global damaged_file_hashes
hash = original_media.GetHash()
if hash in damaged_file_hashes:
return
damaged_file_hashes.add( hash )
if not self._have_shown_human_error_on_this_file:
self._have_shown_human_error_on_this_file = True
if probably_crashy:
message = f'Sorry, this media appears to have a serious problem! To avoid crashes, MPV will not attempt to play it! The file is possibly truncated or otherwise corrupted, but if you think it is good, please send it to hydev for more testing. The specific errors should be written to the log.{media_line}'
HydrusData.DebugPrint( message )
QP.CallAfter( QW.QMessageBox.critical, self, 'Error', f'{message}\n\nThe first error was:\n\n{reason}' )
else:
message = f'A media loaded in MPV appears to have had an error. This may be not a big deal, or it may be a crash. The specific errors should be written after this message. They are not positively known as crashy, but if you are getting crashes, please send the file and these errors to hydev so he can test his end.{media_line}'
HydrusData.DebugPrint( message )
2020-01-16 02:08:23 +00:00
def GetAnimationBarStatus( self ):
2023-10-11 20:46:40 +00:00
if self._file_header_is_loaded and self._LooksLikeALoadError():
self._HandleLoadError()
2020-01-16 02:08:23 +00:00
2023-10-11 20:46:40 +00:00
if self._media is None or not self._file_header_is_loaded or self._currently_in_media_load_error_state:
2020-01-16 02:08:23 +00:00
2023-10-11 20:46:40 +00:00
return None
2020-01-16 02:08:23 +00:00
else:
current_timestamp_s = self._player.time_pos
if current_timestamp_s is None:
current_frame_index = 0
current_timestamp_ms = None
else:
current_timestamp_ms = current_timestamp_s * 1000
num_frames = self._media.GetNumFrames()
2020-02-19 21:48:36 +00:00
if num_frames is None or num_frames == 1:
2020-01-16 02:08:23 +00:00
current_frame_index = 0
else:
2023-03-22 20:28:10 +00:00
current_frame_index = int( round( ( current_timestamp_ms / self._media.GetDurationMS() ) * num_frames ) )
2020-01-16 02:08:23 +00:00
2021-11-17 21:22:27 +00:00
current_frame_index = min( current_frame_index, num_frames - 1 )
2020-01-16 02:08:23 +00:00
2023-03-22 20:28:10 +00:00
current_timestamp_ms = min( current_timestamp_ms, self._media.GetDurationMS() )
2021-11-10 21:53:57 +00:00
2020-01-16 02:08:23 +00:00
paused = self._player.pause
2023-10-11 20:46:40 +00:00
buffer_indices = None
2020-01-16 02:08:23 +00:00
return ( current_frame_index, current_timestamp_ms, paused, buffer_indices )
def GotoPreviousOrNextFrame( self, direction ):
2023-10-11 20:46:40 +00:00
if self._currently_in_media_load_error_state:
return
if not self._file_header_is_loaded:
2020-05-27 21:27:52 +00:00
return
2020-01-16 02:08:23 +00:00
command = 'frame-step'
if direction == 1:
command = 'frame-step'
elif direction == -1:
command = 'frame-back-step'
self._player.command( command )
def HasPlayedOnceThrough( self ):
2020-02-19 21:48:36 +00:00
return self._current_seek_to_start_count > 0
2020-01-16 02:08:23 +00:00
def IsPaused( self ):
2020-01-16 02:08:23 +00:00
2023-10-11 20:46:40 +00:00
if self._currently_in_media_load_error_state:
return True
return self._player.pause
2020-01-16 02:08:23 +00:00
2023-10-11 20:46:40 +00:00
def paintEvent(self, event):
return
2020-02-12 22:50:37 +00:00
def Pause( self ):
2023-10-11 20:46:40 +00:00
if self._currently_in_media_load_error_state:
return
2020-02-12 22:50:37 +00:00
self._player.pause = True
2020-01-16 02:08:23 +00:00
2020-02-12 22:50:37 +00:00
def PausePlay( self ):
2023-10-11 20:46:40 +00:00
if self._currently_in_media_load_error_state:
return
2020-02-12 22:50:37 +00:00
self._player.pause = not self._player.pause
def Play( self ):
2023-10-11 20:46:40 +00:00
if self._currently_in_media_load_error_state:
return
2020-02-12 22:50:37 +00:00
self._player.pause = False
def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ):
2020-02-12 22:50:37 +00:00
command_processed = True
if command.IsSimpleCommand():
2020-02-12 22:50:37 +00:00
2021-08-25 21:59:05 +00:00
action = command.GetSimpleAction()
2020-01-16 02:08:23 +00:00
if action == CAC.SIMPLE_PAUSE_MEDIA:
2020-01-16 02:08:23 +00:00
self.Pause()
elif action == CAC.SIMPLE_PAUSE_PLAY_MEDIA:
2020-01-16 02:08:23 +00:00
2020-02-12 22:50:37 +00:00
self.PausePlay()
2020-01-16 02:08:23 +00:00
2021-08-25 21:59:05 +00:00
elif action == CAC.SIMPLE_MEDIA_SEEK_DELTA:
( direction, duration_ms ) = command.GetSimpleData()
self.SeekDelta( direction, duration_ms )
elif action == CAC.SIMPLE_OPEN_FILE_IN_EXTERNAL_PROGRAM:
2020-01-16 02:08:23 +00:00
2020-02-12 22:50:37 +00:00
if self._media is not None:
2023-05-24 20:44:12 +00:00
self.Pause()
2020-02-12 22:50:37 +00:00
ClientGUIMedia.OpenExternally( self._media )
2020-01-16 02:08:23 +00:00
2023-05-24 20:44:12 +00:00
elif action == CAC.SIMPLE_OPEN_FILE_IN_FILE_EXPLORER:
if self._media is not None:
self.Pause()
ClientGUIMedia.OpenFileLocation( self._media )
2022-01-26 21:57:04 +00:00
elif action == CAC.SIMPLE_CLOSE_MEDIA_VIEWER and self._canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES:
2020-01-16 02:08:23 +00:00
2020-02-12 22:50:37 +00:00
self.window().close()
2020-01-16 02:08:23 +00:00
2022-01-26 21:57:04 +00:00
elif action == CAC.SIMPLE_LAUNCH_MEDIA_VIEWER and self._canvas_type == CC.CANVAS_PREVIEW:
2020-01-16 02:08:23 +00:00
2020-04-08 21:10:11 +00:00
self.launchMediaViewer.emit()
2020-01-16 02:08:23 +00:00
2020-02-12 22:50:37 +00:00
else:
2020-01-22 21:04:43 +00:00
2020-02-12 22:50:37 +00:00
command_processed = False
2020-01-22 21:04:43 +00:00
2020-01-16 02:08:23 +00:00
2020-02-12 22:50:37 +00:00
else:
command_processed = False
2020-01-16 02:08:23 +00:00
2020-02-12 22:50:37 +00:00
return command_processed
2020-01-16 02:08:23 +00:00
2020-02-05 22:55:21 +00:00
2021-02-11 01:59:52 +00:00
def Seek( self, time_index_ms ):
2020-02-19 21:48:36 +00:00
2023-10-11 20:46:40 +00:00
if self._currently_in_media_load_error_state:
return
if not self._file_header_is_loaded:
2021-02-11 01:59:52 +00:00
return
if self._disallow_seek_on_this_file:
return
time_index_s = time_index_ms / 1000
try:
self._player.seek( time_index_s, reference = 'absolute', precision = 'exact' )
2021-02-11 01:59:52 +00:00
except:
self._disallow_seek_on_this_file = True
# on some files, this seems to fail with a SystemError lmaoooo
# with the same elegance, we will just pass all errors
2020-02-19 21:48:36 +00:00
2021-08-25 21:59:05 +00:00
def SeekDelta( self, direction, duration_ms ):
2023-10-11 20:46:40 +00:00
if self._currently_in_media_load_error_state:
return
if not self._file_header_is_loaded:
2021-08-25 21:59:05 +00:00
return
current_timestamp_s = self._player.time_pos
2023-02-15 21:26:44 +00:00
if current_timestamp_s is None:
return
2021-08-25 21:59:05 +00:00
new_timestamp_ms = max( 0, ( current_timestamp_s * 1000 ) + ( direction * duration_ms ) )
2023-03-22 20:28:10 +00:00
if new_timestamp_ms > self._media.GetDurationMS():
2021-08-25 21:59:05 +00:00
new_timestamp_ms = 0
self.Seek( new_timestamp_ms )
def SetCanvasType( self, canvas_type ):
self._canvas_type = canvas_type
2022-01-26 21:57:04 +00:00
if self._canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES:
2021-08-25 21:59:05 +00:00
shortcut_set = 'media_viewer_media_window'
else:
shortcut_set = 'preview_media_window'
self._my_shortcut_handler.SetShortcuts( [ shortcut_set ] )
def SetLogLevel( self, level: str ):
self._player.set_loglevel( level )
2020-02-19 21:48:36 +00:00
2023-03-29 20:57:59 +00:00
def SetMedia( self, media: typing.Optional[ ClientMedia.MediaSingleton ], start_paused = False ):
2020-01-16 02:08:23 +00:00
2020-02-26 22:28:52 +00:00
if media == self._media:
return
2023-06-21 19:50:13 +00:00
global damaged_file_hashes
if media is not None and media.GetHash() in damaged_file_hashes:
self.ClearMedia()
return
2023-10-11 20:46:40 +00:00
self._currently_in_media_load_error_state = False
self._file_header_is_loaded = False
2021-02-11 01:59:52 +00:00
self._disallow_seek_on_this_file = False
2023-02-08 20:19:41 +00:00
self._times_to_play_animation = 0
2020-02-19 21:48:36 +00:00
self._current_seek_to_start_count = 0
2023-10-11 20:46:40 +00:00
self._media = media
2020-01-16 02:08:23 +00:00
if self._media is None:
self._player.pause = True
2023-10-11 20:46:40 +00:00
self._player.loadfile( self._black_png_path )
# old method. this does 'work', but null seems to be subtly dangerous in these cursed lands
'''
2020-01-22 21:04:43 +00:00
if len( self._player.playlist ) > 0:
2020-02-05 22:55:21 +00:00
try:
2023-06-21 19:50:13 +00:00
self._player.command( 'stop' )
# used to have this, it could raise errors if the load failed
# self._player.command( 'playlist-remove', 'current' )
2020-02-05 22:55:21 +00:00
2023-05-24 20:44:12 +00:00
except Exception as e:
HydrusData.PrintException( e )
2020-02-05 22:55:21 +00:00
2023-06-21 19:50:13 +00:00
pass
2020-02-05 22:55:21 +00:00
2020-01-22 21:04:43 +00:00
2023-10-11 20:46:40 +00:00
'''
2020-01-16 02:08:23 +00:00
else:
2023-06-21 19:50:13 +00:00
self._have_shown_human_error_on_this_file = False
2020-01-16 02:08:23 +00:00
hash = self._media.GetHash()
mime = self._media.GetMime()
2023-03-29 20:57:59 +00:00
# some videos have an audio channel that is silent. hydrus thinks these dudes are 'no audio', but when we throw them at mpv, it may play audio for them
# would be fine, you think, except in one reported case this causes scratches and pops and hell whitenoise
# so let's see what happens here
mute_override = not self._media.HasAudio()
2020-01-16 02:08:23 +00:00
client_files_manager = HG.client_controller.client_files_manager
path = client_files_manager.GetFilePath( hash, mime )
self._player.visibility = 'always'
2020-02-26 22:28:52 +00:00
self._stop_for_slideshow = False
2020-02-19 21:48:36 +00:00
2020-01-16 02:08:23 +00:00
self._player.pause = True
2023-10-11 20:46:40 +00:00
if mime in HC.ANIMATIONS and not HG.client_controller.new_options.GetBoolean( 'always_loop_gifs' ):
if mime == HC.ANIMATION_GIF:
self._times_to_play_animation = HydrusAnimationHandling.GetTimesToPlayPILAnimation( path )
elif mime == HC.ANIMATION_APNG:
self._times_to_play_animation = HydrusAnimationHandling.GetTimesToPlayAPNG( path )
2020-01-22 21:04:43 +00:00
try:
self._player.loadfile( path )
except Exception as e:
HydrusData.ShowException( e )
2020-01-16 02:08:23 +00:00
2023-06-28 20:29:14 +00:00
self._player.volume = ClientGUIMediaVolume.GetCorrectCurrentVolume( self._canvas_type )
self._player.mute = mute_override or ClientGUIMediaVolume.GetCorrectCurrentMute( self._canvas_type )
2020-01-16 02:08:23 +00:00
self._player.pause = start_paused
2021-02-11 01:59:52 +00:00
def StopForSlideshow( self, value ):
2020-01-16 02:08:23 +00:00
2021-02-11 01:59:52 +00:00
self._stop_for_slideshow = value
2020-01-16 02:08:23 +00:00
2020-02-05 22:55:21 +00:00
def UpdateAudioMute( self ):
2020-01-22 21:04:43 +00:00
2023-10-11 20:46:40 +00:00
if self._currently_in_media_load_error_state:
return
2023-06-28 20:29:14 +00:00
self._player.mute = ClientGUIMediaVolume.GetCorrectCurrentMute( self._canvas_type )
2020-01-22 21:04:43 +00:00
2020-02-05 22:55:21 +00:00
def UpdateAudioVolume( self ):
2020-01-22 21:04:43 +00:00
2023-10-11 20:46:40 +00:00
if self._currently_in_media_load_error_state:
return
2023-06-28 20:29:14 +00:00
self._player.volume = ClientGUIMediaVolume.GetCorrectCurrentVolume( self._canvas_type )
2020-01-22 21:04:43 +00:00
2020-02-26 22:28:52 +00:00
def UpdateConf( self ):
2020-03-18 21:35:57 +00:00
mpv_config_path = HG.client_controller.GetMPVConfPath()
if not os.path.exists( mpv_config_path ):
default_mpv_config_path = HG.client_controller.GetDefaultMPVConfPath()
if not os.path.exists( default_mpv_config_path ):
HydrusData.ShowText( 'There is no default mpv configuration file to load! Perhaps there is a problem with your install?' )
return
else:
HydrusPaths.MirrorFile( default_mpv_config_path, mpv_config_path )
2020-02-26 22:28:52 +00:00
2022-11-16 21:34:30 +00:00
# let's touch mpv core functions as little ans possible
with open( mpv_config_path, 'rb' ) as f:
conf_content_bytes = f.read()
if self._previous_conf_content_bytes == conf_content_bytes:
return
else:
self._previous_conf_content_bytes = conf_content_bytes
2020-02-26 22:28:52 +00:00
#To load an existing config file (by default it doesn't load the user/global config like standalone mpv does):
2020-03-25 21:15:57 +00:00
load_f = getattr( mpv, '_mpv_load_config_file', None )
2020-04-01 21:51:42 +00:00
if load_f is not None and callable( load_f ):
2020-02-26 22:28:52 +00:00
2020-03-11 21:52:11 +00:00
try:
2020-04-01 21:51:42 +00:00
load_f( self._player.handle, mpv_config_path.encode( 'utf-8' ) ) # pylint: disable=E1102
2020-03-11 21:52:11 +00:00
except Exception as e:
2020-03-25 21:15:57 +00:00
HydrusData.ShowText( 'MPV could not load its configuration file! This was probably due to an invalid parameter value inside the conf. The error follows:' )
2020-03-11 21:52:11 +00:00
HydrusData.ShowException( e )
2020-02-26 22:28:52 +00:00
else:
2020-03-18 21:35:57 +00:00
HydrusData.Print( 'Was unable to load mpv.conf--has the MPV API changed?' )
2020-02-26 22:28:52 +00:00