pythonqtpyqt5pysidepyside2

How to change the audio output device of QMediaPlayer in PySide2/PyQt5


How can I set the audio output of a QMediaPlayer to a specific output in Windows 7 and later?
This was really easy in PySide (using Phonon) but I can't find a way to do it in PySide2.

There are some related questions already, like this old but still not solved one, or this one that asks exactly what I want.
They are both in c++ and its difficult to convert it to PySide2.
The second one is answered with this code:

QMediaService *svc = player->service();
if (svc != nullptr)
{
    QAudioOutputSelectorControl *out = qobject_cast<QAudioOutputSelectorControl *>
                                       (svc->requestControl(QAudioOutputSelectorControl_iid));
    if (out != nullptr)
    {
        out->setActiveOutput(this->ui->comboBox->currentText());
        svc->releaseControl(out);
    }
}

Another one with an attempt to python conversion didn't work also.

I tried to convert them to Python code, but the result was not successful.
Here is my minimal attempt:

import sys
from PySide2 import QtMultimedia
from PySide2.QtCore import QUrl, Qt
from PySide2.QtMultimedia import QMediaPlayer, QMediaContent
from PySide2.QtWidgets import (QPushButton, QSlider, QHBoxLayout, QVBoxLayout,
                               QFileDialog, QStyle, QApplication, QDialog, QComboBox)


class Window(QDialog):
    def __init__(self):
        super().__init__()

        self.out_combo = QComboBox()
        mode = QtMultimedia.QAudio.AudioOutput
        devices = QtMultimedia.QAudioDeviceInfo.availableDevices(mode)
        for item in [(dev.deviceName(), dev) for dev in devices]:
            self.out_combo.addItem(item[0], item[1])
        self.out_combo.currentIndexChanged.connect(self.out_device_changed)

        openBtn = QPushButton('Open file')
        openBtn.clicked.connect(self.open_file)

        self.playBtn = QPushButton()
        self.playBtn.setEnabled(False)
        self.playBtn.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
        self.playBtn.clicked.connect(self.play_file)

        self.slider = QSlider(Qt.Horizontal)
        self.slider.setRange(0, 0)
        self.slider.sliderMoved.connect(self.set_position)

        hor_layout = QHBoxLayout()
        hor_layout.setContentsMargins(0, 0, 0, 0)
        hor_layout.addWidget(openBtn)
        hor_layout.addWidget(self.playBtn)
        hor_layout.addWidget(self.slider)

        ver_layout = QVBoxLayout()
        ver_layout.addWidget(self.out_combo)
        ver_layout.addLayout(hor_layout)

        self.setLayout(ver_layout)

        self.player = QMediaPlayer(None, QMediaPlayer.VideoSurface)

        self.player.stateChanged.connect(self.mediastate_changed)
        self.player.positionChanged.connect(self.position_changed)
        self.player.durationChanged.connect(self.duration_changed)

        self.show()

    def open_file(self):
        file_name, _ = QFileDialog.getOpenFileName(self, "Open file")
        if file_name != '':
            self.player.setMedia(QMediaContent(QUrl.fromLocalFile(file_name)))
            # self.label.setText(basename(file_name))
            self.playBtn.setEnabled(True)

    def play_file(self):
        if self.player.state() == QMediaPlayer.PlayingState:
            self.player.pause()
        else:
            self.player.play()

    def mediastate_changed(self, state):
        if self.player.state() == QMediaPlayer.PlayingState:
            self.playBtn.setIcon(self.style().standardIcon(QStyle.SP_MediaPause))
        else:
            self.playBtn.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))

    def position_changed(self, position):
        self.slider.setValue(position)

    def duration_changed(self, duration):
        self.slider.setRange(0, duration)

    def set_position(self, position):
        self.player.setPosition(position)

    def out_device_changed(self, idx):
        device = self.out_combo.itemData(idx)

        service = self.player.service()
        if service:
            out = service.requestControl("org.qt-project.qt.mediastreamscontrol/5.0")
            if out:
                out.setActiveOutput(self.out_combo.currentText())
                service.releaseControl(out)
            else:
                print("No output found!")


app = QApplication(sys.argv)
window = Window()
sys.exit(app.exec_())

Solution

  • After years of thinking that it was impossible to have output selector for PySide2 (Qt5), I finally solve it!
    Here is the updated working code.
    This works on Windows. For info about other platforms please check and comment..

    import sys
    from PySide2 import QtMultimedia
    from PySide2.QtCore import QUrl, Qt
    from PySide2.QtMultimedia import QMediaPlayer, QMediaContent
    from PySide2.QtWidgets import (QPushButton, QSlider, QHBoxLayout, QVBoxLayout,
                                   QFileDialog, QStyle, QApplication, QDialog, QComboBox)
    
    
    class Window(QDialog):
        def __init__(self):
            super().__init__()
    
            self.player = QMediaPlayer(None, QMediaPlayer.VideoSurface)
    
            self.player.stateChanged.connect(self.media_state_changed)
            self.player.positionChanged.connect(self.position_changed)
            self.player.durationChanged.connect(self.duration_changed)
    
            self.out_combo = QComboBox()
            self.out_combo.currentIndexChanged.connect(self.out_device_changed)
            self.scan_audio_outs()
    
            openBtn = QPushButton('Open file')
            openBtn.clicked.connect(self.open_file)
    
            self.play_btn = QPushButton()
            self.play_btn.setEnabled(False)
            self.play_btn.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
            self.play_btn.clicked.connect(self.play_file)
    
            self.slider = QSlider(Qt.Horizontal)
            self.slider.setRange(0, 0)
            self.slider.sliderMoved.connect(self.set_position)
    
            hor_layout = QHBoxLayout()
            hor_layout.setContentsMargins(0, 0, 0, 0)
            hor_layout.addWidget(openBtn)
            hor_layout.addWidget(self.play_btn)
            hor_layout.addWidget(self.slider)
    
            ver_layout = QVBoxLayout()
            ver_layout.addWidget(self.out_combo)
            ver_layout.addLayout(hor_layout)
    
            self.setLayout(ver_layout)
            self.show()
    
        def scan_audio_outs(self):
            """ Scans the system for available audio outputs and creates a combo box for them
            """
            req = "org.qt-project.qt.audiooutputselectorcontrol/5.0"
            service = self.player.service()
            control = service.requestControl(req)
            if (control is not None and hasattr(control, 'setActiveOutput')
                    and hasattr(control, 'availableOutputs')):
                available_outs = control.availableOutputs()
                available_out_names = [i.split("\\")[1].split(":")[0].title()
                                       for i in available_outs]
                mode = QtMultimedia.QAudio.AudioOutput
                devices = QtMultimedia.QAudioDeviceInfo.availableDevices(mode)
                out_names = iter(i.deviceName() for i in devices)
                self.out_combo.blockSignals(True)
                for idx, name in enumerate(available_out_names):
                    if name.startswith("Default"):
                        self.out_combo.addItem(name, available_outs[idx])
                    else:
                        device_name = next(out_names)
                        self.out_combo.addItem(f"{name}: {device_name}", available_outs[idx])
                self.out_combo.blockSignals(False)
    
        def out_device_changed(self, idx):
            self.player.stop()
            device = self.out_combo.itemData(idx)
            service = self.player.service()
            req = "org.qt-project.qt.audiooutputselectorcontrol/5.0"
            control = service.requestControl(req)
            control.setActiveOutput(device)
    
        def open_file(self):
            file_name, _ = QFileDialog.getOpenFileName(self, "Open file")
            if file_name:
                self.player.setMedia(QMediaContent(QUrl.fromLocalFile(file_name)))
                self.play_btn.setEnabled(True)
    
        def play_file(self):
            if self.player.state() == QMediaPlayer.PlayingState:
                self.player.pause()
            else:
                self.player.play()
    
        def media_state_changed(self, state):
            if state == QMediaPlayer.PlayingState:
                self.play_btn.setIcon(self.style().standardIcon(QStyle.SP_MediaPause))
            else:
                self.play_btn.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
    
        def position_changed(self, position):
            self.slider.setValue(position)
    
        def duration_changed(self, duration):
            self.slider.setRange(0, duration)
    
        def set_position(self, position):
            self.player.setPosition(position)
    
    
    app = QApplication(sys.argv)
    window = Window()
    sys.exit(app.exec_())