How QMediaPlayer.setPosition works for mp3? - python

I got a problem when I try to play a song from a certain position: it does'nt work (the song plays from the beginning).
This problem only occurs when the song is a 'mp3' song, not a 'm4a' one (they're the only format I tested).
The problem seems to come from qt (or PyQt ?) but I'm not sure, here's a minimal example, do I miss something ?
from PyQt5.QtWidgets import QApplication
from PyQt5.QtMultimedia import QMediaContent, QMediaPlayer
from PyQt5.QtCore import QUrl
if __name__ == "__main__":
app = QApplication([])
player = QMediaPlayer()
media_content = QMediaContent(QUrl.fromLocalFile("path_to_my_music_file.mp3"))
player.setMedia(media_content)
player.setPosition(10000)
player.play()
app.exec_()

setMedia() is asynchronous:
Note: This function returns immediately after recording the specified source of the media. It does not wait for the media to finish loading and does not check for errors. Listen for the mediaStatusChanged() and error() signals to be notified when the media is loaded and when an error occurs during loading.
It's possible that, due to the nature of MP3 files, Qt takes some time before being able to correctly seek. Unfortunately, as far as I can tell, this can only be done after some amount of time has passed after playing the file.
A possible solution is to connect to a custom function that delays the setPosition until the media becomes seekable.
This is a subclass that should take care of it (I only tested with mp3 files, so you should try it with other file types to ensure that it works properly).
class Player(QMediaPlayer):
_delayedPos = 0
def setPosition(self, pos):
super().setPosition(pos)
if pos and not self.isSeekable():
self._delayedPos = pos
try:
# ensure that the connection is done only once
self.seekableChanged.connect(self.delaySetPosition, Qt.UniqueConnection)
except:
pass
else:
self._delayedPos = 0
def delaySetPosition(self, seekable):
if seekable:
self.setPosition(self._delayedPos)
try:
# just to be safe, in case the media changes before the previous one
# becomes seekable
self.seekableChanged.disconnect(self.delaySetPosition)
except:
pass

Following #musicamante answer ; a more robust version would be to use one of the following signals :
mediaStatusChanged(QMediaPlayer::MediaStatus status)
mediaChanged(const QMediaContent &media)
currentMediaChanged(const QMediaContent &media)
The best option being mediaStatusChanged which allows you to check the QMediaPlayer::MediaStatus :
from PyQt5.QtWidgets import QApplication
from PyQt5.QtMultimedia import QMediaContent, QMediaPlayer
from PyQt5.QtCore import QUrl
def update_position(status):
if(status == QMediaPlayer.LoadedMedia):
player.setPosition(10000)
player.play()
if __name__ == "__main__":
app = QApplication([])
player = QMediaPlayer()
media_content = QMediaContent(QUrl.fromLocalFile("path_to_my_music_file.mp3"))
player.mediaStatusChanged.connect(update_position)
player.setMedia(media_content)
app.exec_()

Related

MVP player embedded in PySide app not receiving mouse clicks on Windows

I'm building a PySide/Qt based app that with video playback. I found mpv player and the helpful python-mpv library to get it into my Python based app. Everything works great. The issue I'm having is trying to use the player on a Windows machine (no issues on OS X): the player renders, the video starts playing, the sound works, but I cannot CLICK on the on-screen controls. The controls are visible, but clicking on them does nothing. It's as if the widget isn't receiving the mouse clicks, but only on Windows?
I haven't found much else around the internets trying to debug this. The player throws no errors or warnings, so it's hard to know where to start. I'm not normally a Windows dude, either. This person seems to have had the same issue, but I'm not sure if they ever solved it.
Has anyone else had trouble or success getting mpv player to work inside a Qt-based app? Appreciate any insights! Same code I've been playing with below:
#!/usr/bin/env python3
import os
# In order to load mpv-1.dll
os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"]
import mpv
import sys
from PySide2.QtWidgets import *
from PySide2.QtCore import *
class Test(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.container = QWidget(self)
self.setCentralWidget(self.container)
self.container.setAttribute(Qt.WA_DontCreateNativeAncestors)
self.container.setAttribute(Qt.WA_NativeWindow)
player = mpv.MPV(wid=str(int(self.container.winId())),
input_default_bindings=True,
input_vo_keyboard=True)
player.play('./sample-mp4-file.mp4')
if __name__ == '__main__':
# 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.
import locale
locale.setlocale(locale.LC_NUMERIC, 'C')
app = QApplication(sys.argv)
win = Test()
win.setFixedWidth(1200)
win.setFixedHeight(800)
win.show()
sys.exit(app.exec_())
I had this issue, and this is my solution. You can get the "mouse button" event from mpv and call a function.
#player.on_key_press('MOUSE_BTN0')
def mouse_pressed():
print('mouse button pressed')

PyQt5 buttons not connecting

I am trying to build a simple GUI using PyQT5, with 3 buttons to open file browsers and one more to run processing with the selected files, but I can't get my buttons to connect to the functions needed to carry this out.
In the Ctrl class, the _connect_signals function doesn't seem to be calling _input_select. Can anyone help me figure out why?
import sys
# Import QApplication and the required widgets from PyQt5.QtWidgets
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QMainWindow
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QWidget
from PyQt5.QtWidgets import QFileDialog
# Create a subclass of QMainWindow to setup the calculator's GUI
class UI(QMainWindow):
"""App's View (GUI)."""
def __init__(self):
"""View initializer."""
super().__init__()
# Set some main window's properties
self.setFixedSize(300, 150)
# Set the central widget and the general layout
self.generalLayout = QVBoxLayout()
self._centralWidget = QWidget(self)
self.setCentralWidget(self._centralWidget)
self._centralWidget.setLayout(self.generalLayout)
# Create the buttons
self._create_buttons()
def _create_buttons(self):
"""Create the buttons."""
self.buttons = {}
buttons_layout = QVBoxLayout()
# Button text | position on the QVBoxLayout
buttons = {
"Select input file...": 0,
"Select config file...": 1,
"Select output file...": 2,
"Run": 3,
}
# Create the buttons and add them to the grid layout
for btnText, pos in buttons.items():
self.buttons[btnText] = QPushButton(btnText)
buttons_layout.addWidget(self.buttons[btnText], pos)
# Add buttons_layout to the general layout
self.generalLayout.addLayout(buttons_layout)
# Create a Controller class to connect the GUI and the model
class Ctrl:
"""App's Controller."""
def __init__(self, setup, view):
"""Controller initializer."""
self._view = view
self._setup = setup
# Connect signals and slots
self._connect_signals()
def _input_select(self): # Not being called
print("input selection")
options = QFileDialog.Options()
file_select, _ = QFileDialog.getOpenFileNames(
self,
'Select Input File...',
'',
'CSV Files (*.csv);;All Files (*)',
options=options
)
if file_select:
self._setup["input"] = file_select
def _connect_signals(self):
"""Connect signals and slots."""
self._view.buttons["Select input file..."].clicked.connect(self._input_select) # Not working!
# Client code
def main():
"""Main function."""
# Create an instance of `QApplication`
app = QApplication(sys.argv)
# Show the app's GUI
view = UI()
view.show()
setup = {}
# Create instance of the controller
Ctrl(setup=setup, view=view)
# Execute app's main loop
sys.exit(app.exec_())
if __name__ == "__main__":
main()
In case it helps, I started out by butchering this example code from a Real Python tutorial, but must have broken it along the way.
The problem is that you are not keeping any persistent reference to the Ctrl() instance you are creating. This results in python garbage collecting it as soon as the instance is created.
To solve the issue, just assign it to a variable:
def main():
"""Main function."""
# Create an instance of `QApplication`
app = QApplication(sys.argv)
# Show the app's GUI
view = UI()
view.show()
setup = {}
# Create instance of the controller
ctrl = Ctrl(setup=setup, view=view)
# Execute app's main loop
sys.exit(app.exec_())
Some considerations:
while separating logic from interface is usually good practice, it's a concept that needs to be used with care, as sometimes it only makes things more complex than they should be. Most of the times (especially with simple programs), it only makes a bigger codebase without giving any actual benefit: it's harder to read and to debug, and you'll probably end up continuously switching from the logic parts and the ui parts of your code;
your code shows one of the drawback of that concept: when you create the file dialog, you're using self, but in that case it refers to the Ctrl instance, while the argument should be the UI instance instead (which will result in a crash, as Qt will get an unexpected argument type); you can use self._view instead, but, as said, the whole separation in this case just makes things unnecessarily complex;
using strings for dictionary keys that refer to internal objects is rarely a good idea (especially when using long descriptive strings like you did);
when importing more than one element from a module, it's usually better to group them instead of using single line imports: it makes the code tidier and easier to read and inspect: from PyQt5.QtWidgets import (QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget, QFileDialog)

PySide2 UI stops responding when entering a while loop after showing it

I'm having trouble where my QtWidget stops responding if I try to use a while loop after calling the widget.show() method for the QtWidget object. I initially thought the issue was in how I was using the signals and slots. I had created my own signal with new_data = Signal(float) and I was sampling data at an interval established by a time.sleep() call within that while loop and emitting the new_data signal each time the data was sampled. That was connected to a method in my QtWidget that just set the text of a label in the QtWidget to display the new data.
However, after some testing I found that if I ONLY try to print("in loop") inside that while loop, I get the same behavior. The QtWidget object stops responding. What is the proper way to update PySide2 interface at a periodic interval from outside the interface object? Can I perhaps run the interface as a process and feed it with updated data with a queue? I imaging that is possible, but am having trouble finding an example. The interface is only one piece of this application primarily made in Python and I will have multiple processes and multiple queues besides the Qt interface. Here is the code:
import sys
from PySide2.QtUiTools import QUiLoader
from PySide2.QtWidgets import QApplication, QWidget
from PySide2.QtCore import QFile, QObject, Signal, Slot
import time
import NI9213
class MyDaq(QObject):
new_daq_data = Signal(float)
def __init__(self):
QObject.__init__(self)
daq_channels = "cDAQ1Mod2/ai0"
self.daq = NI9213.NI9213(channels=daq_channels)
def sample_daq(self):
data = self.daq.read_all()
self.new_daq_data.emit(data)
class DigitalDisplay(QWidget):
def __init__(self):
#Initialize the QWidget object used to create the user interface
QWidget.__init__(self)
#Load the user interface
designer_file = QFile("signal_digital_display.ui")
designer_file.open(QFile.ReadOnly)
loader = QUiLoader()
self.ui = loader.load(designer_file, self)
designer_file.close()
#Add title to the UI window
self.setWindowTitle("Digital Display")
self.mode = 'run'
self.ui.stopButton.clicked.connect(self.stopMode)
self.sampling_period = 0.1
#Slot(float)
def refresh_data(self, data):
self.ui.label.setText(str(data))
def stopMode(self):
self.mode = 'stop'
if __name__ == "__main__":
app = QApplication(sys.argv)
digital_display = DigitalDisplay()
digital_display.show()
## data = MyDaq()
## data.new_daq_data.connect(digital_display.refresh_data)
##
while(digital_display.mode=='run'):
print("after display.show")
## data.sample_daq()
time.sleep(digital_display.sampling_period)
sys.exit(app.exec_())
The problem here is that you have an infinite loop before you start the Qt event loop.
if __name__ == "__main__":
app = QApplication(sys.argv) # [1]
digital_display = DigitalDisplay()
digital_display.show() # [2]
while(digital_display.mode=='run'):
print("after display.show")
time.sleep(digital_display.sampling_period) # [3]
sys.exit(app.exec_()) # [4]
[1] This creates the QApplication object. It does not start the event loop yet.
[2] Create your widget and open a new window to display it. The operating system will create events for the window, but they have no effect on the application itself until we start the event loop.
[3] digital_display.mode will never change. This is an infinite loop and Python will never advance past this point.
[4] Here we start the application event loop and close the process once the application has finished. But we never get here.
What you should do instead is to create a QTimer on the DigitalDisplay widget that periodically fires a signal that can be connected to data.sample_daq.

Always get 0 when using QMediaPlayer.duration()

Here is my code:
import sys
from PyQt5.Qt import QUrl
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QMediaPlaylist
from PyQt5.QtWidgets import QApplication, QWidget
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.playlist = QMediaPlaylist(self)
self.player = QMediaPlayer(self)
self.player.setPlaylist(self.playlist)
self.playlist.addMedia(QMediaContent(
QUrl.fromLocalFile('mypath')))
self.playlist.addMedia(QMediaContent(
QUrl.fromLocalFile('mypath')))
self.playlist.setPlaybackMode(QMediaPlaylist.Loop)
self.playlist.setCurrentIndex(1)
self.player.setVolume(80)
self.player.play()
self.player.durationChanged.connect(self.print_durantion)
print(self.player.duration()) # 0
def print_durantion(self, d):
print(d) # never printed
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
I'm trying to get the duration of the file, but always get 0. It says in QtAssistant that the value may not be available when initial playback begins and tells us to use durantionChanged to receive status notifications.
But how will the durationChanged signal be emitted if duration is always 0(the value d is never printed)?
Is there any way to get duration when a media file is played? Any helps would be appreciated. (I tested the above code on MacOS)
You are attaching the durationChanged signal after you select the media. This signal is triggered only once as the media is selected and not after.
If you place it before setCurrentIndex it should work as expected, but you may need to attaching it before linking the playlist.
Here is a working mediaplayer demo application in PyQt5 to confirm the signal works for your current PyQt5 install.

Python and PyQt: on exit confirmation box callEvent() it's invoched two times

I'm a newbie to Python and PyQt. I've tried to manage closeEvent to ask before to close mainwindow, but this work well only from 'X' button. From the QMEssageBox created to ask the user, callEvent() it's called two times.
This is the relevant part of the code :
self.ui.actionChiudi.triggered.connect(self.close)
def closeEvent(self, event):
#check presence of data in the table
if self.csvViewer.rowCount() > 0:
print event # only to analyze the caller
#show a warning
Error = QtGui.QMessageBox()
Error.setIcon(QtGui.QMessageBox.Question)
Error.setWindowTitle('ATTENZIONE !!')
Error.setInformativeText(u"Sei sicuro di voler uscire?")
Error.setStandardButtons(QtGui.QMessageBox.Ok | QtGui.QMessageBox.Cancel)
ret = Error.exec_()
if ret == QtGui.QMessageBox.Ok:
event.accept()
else:
event.ignore()
else:
#close directly
event.accept()
'actionChiudi' is the menù item in the main menù.
For what I can understand, when use 'X' button, the function close() it's called only one time directly from mainwindow object, then close my app.
When use the menù item, the function create the new object 'QMessageBox' then call 'closeEvent()' one time for this object, then recall the same function for the mainwindow object. If this is correct, I don't know how to manage this.
Thanks in advance for any help!
thankyou for your tips about howto construct a good example of code. Sorry but I'm new there, then I did not succeed in making a good example. But, in the meantime, I was thinking about the problem and, at the end, I've solved this with a little trick.
I've added a global variable that count if you are closing the main window, then at the other calls to closeEvent() avoid to recall the test procedure.
I've tried to create a class for the generation of QMessageBox external to main class, then to override closeEvent() of this object, but don't work. The only way that I've found is with global variable. The program continue to call two times closeEvent (one for the QMessageBox and one for the mainwindow) but now, the second time ignore to recall the test. It's a trick, it's not elegant, but it works for me.
The piece of code now is this :
#!/usr/bin/env python
#-*- coding:utf-8 -*-
import sys
import os
import datetime
import inspect
import csv
import codecs
import pymssql
from PyQt4 import QtGui, QtCore, Qt
import mainwindow
#import _winreg only on Microsoft platforms
import platform
winos = True
if os.name == 'nt' and platform.system() == 'Windows':
import _winreg
#other stuff needed under Windows
import _mssql
import decimal
import uuid
winos = True
else:
winos = False
#other global variables
liccode = False
closemain = False
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
QtGui.QDialog.__init__(self, parent)
super(MainWindow, self).__init__(parent)
self.ui = mainwindow.Ui_MainWindow()
self.ui.setupUi(self)
# some stuff and widgets to visualize in the main window
#connettors to menù functions; leave only the closing one
self.ui.actionChiudi.triggered.connect(self.close)
#override of closeEvent - there I think, it's better to work to separate
#closeEvent for mainwindow from general closeEvent. But how?
def closeEvent(self, event):
global closemain
#make evaluation test to decide how to work. Close directly or no?
if self.csvViewer.rowCount() > 0 and not closemain:
print event
#show a warning
Error = QtGui.QMessageBox()
Error.setIcon(QtGui.QMessageBox.Question)
Error.setWindowTitle('WARNING !!')
Error.setInformativeText(u"Are you sure to leave?")
Error.setStandardButtons(QtGui.QMessageBox.Ok | QtGui.QMessageBox.Cancel)
ret = Error.exec_()
if ret == QtGui.QMessageBox.Ok:
closemain = True
event.accept()
else:
event.ignore()
else:
#close directly
event.accept()
#start application and create main
app = QtGui.QApplication(sys.argv)
my_mainWindow = MainWindow()
my_mainWindow.show()
sys.exit(app.exec_())
In any case, any advice is well accepted to make a better code. Thank you all!

Categories