446 lines
17 KiB
Python
446 lines
17 KiB
Python
import vlc
|
|
import sys
|
|
import os.path
|
|
import vlc
|
|
from PyQt5 import QtWidgets, QtCore, QtGui
|
|
import subprocess
|
|
from video import VideoModel
|
|
from superqt import QLabeledRangeSlider
|
|
from actions import UndoableAction, HistoryManager
|
|
from typing import List
|
|
import os
|
|
from datetime import datetime
|
|
|
|
|
|
history = HistoryManager()
|
|
|
|
class Player(QtWidgets.QMainWindow):
|
|
"""A simple Media Player using VLC and Qt
|
|
"""
|
|
def __init__(self, master=None):
|
|
QtWidgets.QMainWindow.__init__(self, master)
|
|
self.setWindowTitle("Media Player")
|
|
|
|
# creating a basic vlc instance
|
|
self.instance = vlc.Instance()
|
|
# creating an empty vlc media player
|
|
self.mediaplayer = self.instance.media_player_new()
|
|
self.createUI()
|
|
self.isPaused = False
|
|
|
|
|
|
def createUI(self):
|
|
"""Set up the user interface, signals & slots
|
|
"""
|
|
self.widget = QtWidgets.QWidget(self)
|
|
self.setCentralWidget(self.widget)
|
|
|
|
# In this widget, the video will be drawn
|
|
if sys.platform == "darwin": # for MacOS
|
|
self.videoframe = QtWidgets.QMacCocoaViewContainer(0)
|
|
else:
|
|
self.videoframe = QtWidgets.QFrame()
|
|
self.palette = self.videoframe.palette()
|
|
self.palette.setColor (QtGui.QPalette.Window,
|
|
QtGui.QColor(0,0,0))
|
|
self.videoframe.setPalette(self.palette)
|
|
self.videoframe.setAutoFillBackground(True)
|
|
|
|
self.positionslider = QtWidgets.QSlider(QtCore.Qt.Horizontal, self)
|
|
self.positionslider.setToolTip("Position")
|
|
self.positionslider.setMaximum(1000)
|
|
|
|
def mousePressEvent(event):
|
|
if event.button() == QtCore.Qt.LeftButton:
|
|
val = self.pixelPosToRangeValue(event.pos())
|
|
self.setValue(val)
|
|
|
|
self.positionslider.sliderMoved.connect(self.setPosition)
|
|
self.positionslider.sliderPressed.connect(lambda *args: print(args))
|
|
|
|
|
|
self.hbuttonbox = QtWidgets.QHBoxLayout()
|
|
self.playbutton = QtWidgets.QPushButton("Play")
|
|
self.hbuttonbox.addWidget(self.playbutton)
|
|
self.playbutton.clicked.connect(self.PlayPause)
|
|
|
|
self.hbuttonbox.addStretch(1)
|
|
self.setVolume(100)
|
|
|
|
self.videoplayerlayout = QtWidgets.QVBoxLayout()
|
|
self.videoplayerlayout.addWidget(self.videoframe)
|
|
self.videoplayerlayout.addWidget(self.positionslider)
|
|
self.videoplayerlayout.addLayout(self.hbuttonbox)
|
|
|
|
|
|
self.videoplayerlayoutWidget = QtWidgets.QWidget();
|
|
self.videoplayerlayoutWidget.setLayout(self.videoplayerlayout);
|
|
|
|
|
|
|
|
import dataclasses
|
|
def move_current_frame(d_frames):
|
|
before = self.mediaplayer.get_time()
|
|
|
|
mspf = int(1000 // (self.mediaplayer.get_fps() or 25))
|
|
d_time_ms = d_frames * mspf
|
|
target = self.mediaplayer.get_time() + d_time_ms
|
|
result = self.mediaplayer.set_time(target)
|
|
|
|
print(before, target, result, self.mediaplayer.get_time())
|
|
|
|
|
|
def pp():
|
|
self.PlayPause()
|
|
print(self.mediaplayer.get_time())
|
|
|
|
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("Space"), self)
|
|
self.shortcut.activated.connect(pp)
|
|
|
|
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("Right"), self)
|
|
self.shortcut.activated.connect(lambda : move_current_frame(1))
|
|
|
|
|
|
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("Left"), self)
|
|
self.shortcut.activated.connect(lambda : move_current_frame(-1))
|
|
|
|
|
|
def export_frame():
|
|
print("XX")
|
|
t = self.mediaplayer.get_time()
|
|
print(t)
|
|
ms = t % 1000
|
|
t = t // 1000
|
|
s = t % 60
|
|
t = t // 60
|
|
m = t % 60
|
|
t = t // 60
|
|
h = t % 60
|
|
t = t // 60
|
|
t_str = f"{h:02d}:{m:02d}:{s:02d}.{ms:03d}"
|
|
print("Exporting: ", self.filename, t_str)
|
|
import os
|
|
from datetime import timedelta
|
|
|
|
def apply(filename, t_str):
|
|
base_dir = os.path.expanduser(os.path.join("~", "Pictures", "video_exports"))
|
|
|
|
print("W", filename, t_str)
|
|
if not os.path.exists(base_dir):
|
|
os.makedirs(base_dir)
|
|
print("W", filename, t_str)
|
|
begin = datetime.now()
|
|
base_name = os.path.splitext(os.path.basename(filename))[0]
|
|
|
|
print("W", filename, t_str)
|
|
print(base_name[:13])
|
|
frame_datetime = datetime.strptime(base_name[:13], "%Y%m%d_%H%M") + timedelta(hours=h, minutes=m, seconds=s, milliseconds=ms)
|
|
print(base_name[:13])
|
|
frame_datetime_str = frame_datetime.strftime("%Y:%m:%d_%H:%M:%S")
|
|
print(base_name[:13])
|
|
|
|
print("W", filename, t_str)
|
|
out = os.path.join(base_dir,
|
|
base_name +
|
|
f"{h:02d}{m:02d}{s:02d}{ms:03d}.png")
|
|
print(out)
|
|
subprocess.call([
|
|
"ffmpeg",
|
|
"-ss", t_str,
|
|
"-i", filename,
|
|
"-vframes", "1",
|
|
"-c:v", "png",
|
|
out
|
|
])
|
|
subprocess.call([
|
|
"exiftool",
|
|
f"-CreateDate={frame_datetime_str}",
|
|
f"-DateTimeOriginal={frame_datetime_str}",
|
|
f"-PNG:CreationTime={frame_datetime_str}",
|
|
"-overwrite_original",
|
|
out
|
|
])
|
|
print("Duration: ", datetime.now() - begin)
|
|
from video import thumbnailer
|
|
|
|
thumbnailer.pool.apply_async(apply, args=(self.filename, t_str))
|
|
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+e"), self)
|
|
self.shortcut.activated.connect(lambda : export_frame())
|
|
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("f"), self)
|
|
self.shortcut.activated.connect(lambda : self.mediaplayer.toggle_fullscreen())
|
|
|
|
def slowmo():
|
|
if self.mediaplayer.get_rate() < 0.7:
|
|
print("rate", 1)
|
|
self.mediaplayer.set_rate(1)
|
|
else:
|
|
print("rate", .5)
|
|
self.mediaplayer.set_rate(0.5)
|
|
|
|
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("s"), self)
|
|
self.shortcut.activated.connect(slowmo)
|
|
|
|
def change_size(diff):
|
|
self.icon_size += diff
|
|
print(self.icon_size < 64)
|
|
if self.icon_size < 64:
|
|
self.icon_size = 64
|
|
if self.icon_size > 256:
|
|
self.icon_size = 256
|
|
print(self.icon_size)
|
|
self.list_view.setIconSize(QtCore.QSize(self.icon_size, self.icon_size))
|
|
|
|
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("+"), self)
|
|
self.shortcut.activated.connect(lambda : change_size(64))
|
|
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("-"), self)
|
|
self.shortcut.activated.connect(lambda : change_size(-64))
|
|
from typing import Optional
|
|
def undo():
|
|
history.undo(self.model)
|
|
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+z"), self)
|
|
self.shortcut.activated.connect(lambda : undo())
|
|
def redo():
|
|
history.redo(self.model)
|
|
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+Shift+z"), self)
|
|
self.shortcut.activated.connect(lambda : redo())
|
|
|
|
|
|
def set_rating(value):
|
|
selected = self.list_view.selectedIndexes()
|
|
|
|
stars = None
|
|
actions = []
|
|
for i, index in enumerate(sorted(selected, key=lambda r: -r.row())):
|
|
video = self.model.video_files[index.row()]
|
|
from actions import SetVideoStars
|
|
stars = SetVideoStars((
|
|
video.fn,
|
|
video.metadata.rejected,
|
|
video.metadata.rating,
|
|
False if value >= 0 else not video.metadata.rejected,
|
|
value if value >= 0 else video.metadata.rating
|
|
), stars)
|
|
stars.run(self.model)
|
|
actions.append(stars)
|
|
history.apply(self.model, *actions)
|
|
|
|
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("0"), self)
|
|
self.shortcut.activated.connect(lambda : set_rating(0))
|
|
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("1"), self)
|
|
self.shortcut.activated.connect(lambda : set_rating(1))
|
|
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("2"), self)
|
|
self.shortcut.activated.connect(lambda : set_rating(2))
|
|
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("3"), self)
|
|
self.shortcut.activated.connect(lambda : set_rating(3))
|
|
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("4"), self)
|
|
self.shortcut.activated.connect(lambda : set_rating(4))
|
|
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("5"), self)
|
|
self.shortcut.activated.connect(lambda : set_rating(5))
|
|
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("r"), self)
|
|
self.shortcut.activated.connect(lambda : set_rating(-1))
|
|
|
|
def link():
|
|
directory = QtWidgets.QFileDialog.getExistingDirectory(
|
|
self, "Open File", os.path.expanduser('~'))
|
|
|
|
if not os.path.isdir(directory):
|
|
QtWidgets.QMessageBox.warning(
|
|
self,
|
|
"Not a directory",
|
|
"The selected item was not a directory. "
|
|
"You need to select an empty folder. ")
|
|
return
|
|
|
|
if os.listdir(directory):
|
|
QtWidgets.QMessageBox.warning(
|
|
self,
|
|
"Non-empty directory selected",
|
|
"The selected directory is not empty. "
|
|
"You need to select an empty folder. ")
|
|
return
|
|
|
|
print(directory)
|
|
failure = False
|
|
for index in self.list_view.selectedIndexes():
|
|
video = self.model.video_files[index.row()]
|
|
failure = failure or (
|
|
0 != subprocess.call([
|
|
"ln",
|
|
"-s",
|
|
video.fn,
|
|
os.path.join(directory, os.path.basename(video.fn)),
|
|
])
|
|
)
|
|
if failure:
|
|
QtWidgets.QMessageBox.warning(
|
|
self,
|
|
"Linkage failure",
|
|
"Some files could not be linked.")
|
|
|
|
|
|
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+L"), self)
|
|
self.shortcut.activated.connect(link)
|
|
|
|
def delete():
|
|
from actions import Trash
|
|
selected = self.list_view.selectedIndexes()
|
|
response = QtWidgets.QMessageBox.question(self, "Delete Items", "Do you really want to delete %d items?" % len(selected), defaultButton=QtWidgets.QMessageBox.No)
|
|
if response == QtWidgets.QMessageBox.Yes:
|
|
actions = []
|
|
for i, index in enumerate(sorted(selected, key=lambda r: -r.row())):
|
|
video = self.model.video_files[index.row()]
|
|
action = Trash(video.fn, actions[-1] if len(actions) else None)
|
|
actions.append(action)
|
|
history.apply(self.model, *actions)
|
|
|
|
|
|
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("Del"), self)
|
|
self.shortcut.activated.connect(lambda : delete())
|
|
|
|
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("F5"), self)
|
|
self.shortcut.activated.connect(lambda : self.model.reload())
|
|
|
|
self.model = VideoModel()
|
|
|
|
def open_file(index):
|
|
self.OpenFile(self.model.video_files[index.row()].fn)
|
|
|
|
self.list_view = QtWidgets.QListView()
|
|
self.list_view.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.ExtendedSelection)
|
|
self.list_view.doubleClicked.connect(open_file)
|
|
self.list_view.setViewMode(QtWidgets.QListView.IconMode)
|
|
self.list_view.setResizeMode(QtWidgets.QListView.Adjust)
|
|
self.list_view.setFlow(QtWidgets.QListView.LeftToRight)
|
|
self.list_view.setMovement(QtWidgets.QListView.Snap)
|
|
self.list_view.setModel(self.model)
|
|
# self.list_view.setGridSize(QtCore.QSize(256 + 10, 256 + 10))
|
|
self.icon_size = 256
|
|
self.list_view.setIconSize(QtCore.QSize(self.icon_size, self.icon_size))
|
|
self.list_view.setWordWrap(True)
|
|
|
|
self.hboxlayout = QtWidgets.QSplitter()
|
|
self.hboxlayout.addWidget(self.list_view)
|
|
self.hboxlayout.addWidget(self.videoplayerlayoutWidget)
|
|
|
|
self.filter_slider = QLabeledRangeSlider(QtCore.Qt.Horizontal)
|
|
self.filter_slider.setRange(0, 6)
|
|
self.filter_slider.setValue((0, 6))
|
|
self.filter_slider.setMaximum(6)
|
|
self.filter_slider.setMinimum(-1)
|
|
|
|
def set_rating_filter(v):
|
|
self.model.filter_rating = v
|
|
self.filter_slider.sliderMoved.connect(set_rating_filter)
|
|
self.parentLayout = QtWidgets.QVBoxLayout()
|
|
self.parentLayout.addWidget(self.filter_slider, 0)
|
|
self.parentLayout.addWidget(self.hboxlayout, 2)
|
|
self.widget.setLayout(self.parentLayout)
|
|
|
|
exit = QtWidgets.QAction("&Exit", self)
|
|
exit.triggered.connect(sys.exit)
|
|
menubar = self.menuBar()
|
|
filemenu = menubar.addMenu("&File")
|
|
filemenu.addAction(exit)
|
|
|
|
self.timer = QtCore.QTimer(self)
|
|
self.timer.setInterval(200)
|
|
self.timer.timeout.connect(self.updateUI)
|
|
|
|
def PlayPause(self):
|
|
"""Toggle play/pause status
|
|
"""
|
|
if self.mediaplayer.is_playing():
|
|
self.mediaplayer.pause()
|
|
self.playbutton.setText("Play")
|
|
self.isPaused = True
|
|
else:
|
|
if self.mediaplayer.play() == -1:
|
|
self.OpenFile()
|
|
return
|
|
self.mediaplayer.play()
|
|
self.playbutton.setText("Pause")
|
|
self.timer.start()
|
|
self.isPaused = False
|
|
|
|
def Stop(self):
|
|
"""Stop player
|
|
"""
|
|
self.mediaplayer.stop()
|
|
self.playbutton.setText("Play")
|
|
|
|
def OpenFile(self, filename=None):
|
|
"""Open a media file in a MediaPlayer
|
|
"""
|
|
if filename is None:
|
|
return
|
|
filename = QtWidgets.QFileDialog.getOpenFileName(self, "Open File", os.path.expanduser('~'))
|
|
if not filename:
|
|
return
|
|
|
|
# create the media
|
|
if sys.version < '3':
|
|
filename = unicode(filename)
|
|
self.filename = filename
|
|
self.media = self.instance.media_new(filename)
|
|
# put the media in the media player
|
|
self.mediaplayer.set_media(self.media)
|
|
|
|
# parse the metadata of the file
|
|
self.media.parse()
|
|
# set the title of the track as window title
|
|
self.setWindowTitle(self.media.get_meta(0))
|
|
|
|
# the media player has to be 'connected' to the QFrame
|
|
# (otherwise a video would be displayed in it's own window)
|
|
# this is platform specific!
|
|
# you have to give the id of the QFrame (or similar object) to
|
|
# vlc, different platforms have different functions for this
|
|
if sys.platform.startswith('linux'): # for Linux using the X Server
|
|
self.mediaplayer.set_xwindow(int(self.videoframe.winId()))
|
|
elif sys.platform == "win32": # for Windows
|
|
self.mediaplayer.set_hwnd(self.videoframe.winId())
|
|
elif sys.platform == "darwin": # for MacOS
|
|
self.mediaplayer.set_nsobject(self.videoframe.winId())
|
|
self.PlayPause()
|
|
|
|
def setVolume(self, Volume):
|
|
"""Set the volume
|
|
"""
|
|
self.mediaplayer.audio_set_volume(Volume)
|
|
|
|
def setPosition(self, position):
|
|
"""Set the position
|
|
"""
|
|
# setting the position to where the slider was dragged
|
|
self.mediaplayer.set_position(position / 1000.0)
|
|
# the vlc MediaPlayer needs a float value between 0 and 1, Qt
|
|
# uses integer variables, so you need a factor; the higher the
|
|
# factor, the more precise are the results
|
|
# (1000 should be enough)
|
|
|
|
def updateUI(self):
|
|
"""updates the user interface"""
|
|
# setting the slider to the desired position
|
|
self.positionslider.setValue(int(self.mediaplayer.get_position() * 1000))
|
|
|
|
if not self.mediaplayer.is_playing():
|
|
# no need to call this function if nothing is played
|
|
self.timer.stop()
|
|
if not self.isPaused:
|
|
# after the video finished, the play button stills shows
|
|
# "Pause", not the desired behavior of a media player
|
|
# this will fix it
|
|
self.Stop()
|
|
|
|
if __name__ == "__main__":
|
|
app = QtWidgets.QApplication(sys.argv)
|
|
|
|
|
|
|
|
player = Player()
|
|
player.show()
|
|
player.resize(640, 480)
|
|
if sys.argv[1:]:
|
|
player.OpenFile(sys.argv[1])
|
|
sys.exit(app.exec_())
|