0

I need to play a .mov video (ProRes4444) with alpha channel in a scene. The scene has a background image and I need to use the alpha channel of the video so it overlays on the background.

If I open the video normally with QMediaPlayer, the alpha channel appears in black.

screen with background pic & video with black alpha:

screen with background pic & video with black alpha

How can I make the output of the QMediaPlayer (QGraphicsVideoItem) respect the alpha and make the overlay effect possible?

The closest I got to the answer based on online research is code in cpp that I've found that shows the necessity to create a subclass of a QAbstractVideoSurface that receives videoframes converts to ARGB, then forwards those to a QLabel that displays them.

Displaying a video with an alpha channel using qt

I've also tried that unsuccessfully. Is this the right course or I'm just missing something simple on my current code?

EDIT: Link to files (background image and video .mov) https://drive.google.com/drive/folders/1LIZzTg1E8wkaD0YSvkkcfSATdlDTggyh?usp=sharing

import sys
from PyQt5.QtMultimedia import *
from PyQt5.QtMultimediaWidgets import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *


class VideoWindow(QMainWindow):
     def __init__(self):
        super(VideoWindow, self).__init__()
        self.setWindowTitle('QMediaPlayer TEST')
        self.resize(1920, 1080)

        self.vista = QGraphicsView(self)
        self.vista.setGeometry(QRect(0, 0, 1920, 1080))

        self.scene = QGraphicsScene(self.vista)
        self.scene.setSceneRect(0, 0, 1920, 1080)
        self.vista.setScene(self.scene)

        self.graphvitem1 = QGraphicsVideoItem()

#SET BACKGROUND IMAGE ON SCENE
        self.tempImg = QPixmap("/Users/elemental/Desktop/pyvids/fons.jpeg")
        self.tempImg = self.tempImg.scaled(self.scene.width(), self.scene.height())
        self.graphicsPixmapItem = QGraphicsPixmapItem(self.tempImg)
        self.scene.addItem(self.graphicsPixmapItem)

#SET VIDEO 1 WITH LOOP
        self.mediaPlayer1 = QMediaPlayer(None, QMediaPlayer.VideoSurface)
        self.mediaPlayer1.setVideoOutput(self.graphvitem1)

        self.playlist1 = QMediaPlaylist(self)
        self.playlist1.addMedia(QMediaContent(QUrl.fromLocalFile("/Users/elemental/Desktop/pyvids/vida1.mov")))
        self.playlist1.setCurrentIndex(1)
        self.playlist1.setPlaybackMode(QMediaPlaylist.CurrentItemInLoop)
        self.mediaPlayer1.setPlaylist(self.playlist1)

        self.graphvitem1.setPos(500, 100)
        self.graphvitem1.setSize(QSizeF(1000, 500))
        self.scene.addItem(self.graphvitem1)

        self.mediaPlayer1.play()
        self.vista.show()


if __name__ == '__main__':
     app = QApplication([])
     window = VideoWindow()
     window.show()
     sys.exit(app.exec_())
noi
  • 11
  • 2
  • That seems to be doable in Python (but with some performance loss). Anyway, that is a pretty peculiar format, do you have a basic file that you could share with us so if anybody could port it could also test it? – musicamante Jul 19 '21 at 20:46
  • Note: you said that you "tried that unsuccessfully", then I suggest you to provide that attempt, as we might be able to understand if you did it properly or not. That said, one question: is that video a "static" and predefined animation? Because if that's the case, then you might opt for other and simpler solutions, like using a GIF with QMovie, or a list of PNG images. – musicamante Jul 19 '21 at 20:57
  • @musicamante I'm still trying to figure out the best container and codec, but so far I'm trying mov with ProRes4444. If there's any other better for this purpose, I can switch. The goal is to build a big 4k main UI that plays different (and smaller) hi-res animations/videos with alpha channels in different positions, but also, I have to be able to pause them to a specific point and resume play. If png has lower impact on cpu,gpu then it's way to explore for sure. But also, I am open to upgrading to any necessary hardware to avoid any issues with performance if I end up doing it with videofiles – noi Jul 19 '21 at 21:19
  • @musicamante When I say "tried unsuccessfully" I mean that I've tried to "translate" the cpp code to python, but my knowledge of PyQt is still very limited, so I got stuck halfway and decided to abandon the route and ask for advice before dedicating hours onto something I wasn't sure it would've been successful nor practical. So, "Unsuccessful"... – noi Jul 19 '21 at 22:02
  • @musicamante added a link to download the video and background image. Thank you very much for your input! – noi Jul 19 '21 at 22:06

1 Answers1

0

From what I can see, QVideoWidget doesn't support alpha channels by default, so it falls back to the "basic" black background.

But, implementation seems possible, by properly subclassing QAbstractVideoSurface.

Consider that the following code is experimental, my knowledge of QMediaPlayer and the Qt video surface isn't that deep (the former is an abstract for multiple platforms and multiple libraries that can behave very differently on different configurations), and I could only test it on two Linux platforms, so I don't know how it behaves under Windows nor MacOS.

The assumption is that the video surface provides a default dedicated QWidget subclass (VideoWidget) unless another class with a suitable setImage is provided, and updates its image whenever the media player requires it.

Note that I only tested it with a couple of videos (including the provided one), and further testing might be required.

from PyQt5.QtMultimedia import *
from PyQt5.QtMultimediaWidgets import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class VideoWidget(QWidget):
    image = QImage()
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def setImage(self, image):
        self.image = image
        self.update()

    def sizeHint(self):
        return QSize(640, 480)

    def paintEvent(self, event):
        qp = QPainter(self)
        # ensure that smooth transformation is used while scaling pixmaps
        qp.setRenderHints(qp.SmoothPixmapTransform)

        # provide compliancy with background set using stylesheets, see:
        # https://doc.qt.io/qt-5/stylesheet-reference.html#qwidget-widget
        opt = QStyleOption()
        opt.initFrom(self)
        self.style().drawPrimitive(QStyle.PE_Widget, opt, qp, self)

        # draw the image, scaled to the widget size; if you need fixed sizes
        # or keep aspect ratio, implement this (or the widget) accordingly
        qp.drawImage(self.rect(), self.image, self.image.rect())


class AlphaVideoDrawer(QAbstractVideoSurface):
    def __init__(self, videoWidget=None, widgetOptions=None):
        super().__init__()
        if videoWidget:
            if not hasattr(videoWidget, 'setImage'):
                raise NotImplementedError(
                    'setImage() must be implemented for videoWidget!')
        else:
            if not isinstance(widgetOptions, dict):
                widgetOptions = {}
            elif not 'styleSheet' in widgetOptions:
                # just a default background for testing purposes
                widgetOptions = {'styleSheet': 'background: darkGray;'}
            videoWidget = VideoWidget(**widgetOptions)
        self.videoWidget = videoWidget

        # QVideoFrame.image() has been introduced since Qt 5.15
        version, majVersion, minVersion = map(int, QT_VERSION_STR.split('.'))
        if version < 6 and majVersion < 15:
            self.imageFromFrame = self._imageFromFrameFix
        else:
            self.imageFromFrame = lambda frame: frame.image()

    def _imageFromFrameFix(self, frame):
        clone_frame = QVideoFrame(frame)
        clone_frame.map(QAbstractVideoBuffer.ReadOnly)
        image = QImage(
            clone_frame.bits(), frame.width(), frame.height(), frame.bytesPerLine(), 
            QVideoFrame.imageFormatFromPixelFormat(frame.pixelFormat()))
        clone_frame.unmap()
        return image

    def supportedPixelFormats(self, type):
        return [QVideoFrame.Format_ARGB32]

    def present(self, frame: QVideoFrame):
        if frame.isValid():
            self.videoWidget.setImage(self.imageFromFrame(frame))

        if self.surfaceFormat().pixelFormat() != frame.pixelFormat() or \
            self.surfaceFormat().frameSize() != frame.size():
                self.setError(QAbstractVideoSurface.IncorrectFormatError)
                self.stop()
                return False
        else:
            return True


class AlphaVideoTest(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setStyleSheet('''
            QFrame#mainFrame {
                background: green;
            }
        ''')

        mainFrame = QFrame(objectName='mainFrame')
        self.setCentralWidget(mainFrame)

        layout = QVBoxLayout(mainFrame)
        self.playButton = QPushButton('Play', checkable=True)
        layout.addWidget(self.playButton)

        self.drawer = AlphaVideoDrawer()
        layout.addWidget(self.drawer.videoWidget)

        self.mediaPlayer1 = QMediaPlayer(self, QMediaPlayer.VideoSurface)
        self.playlist = QMediaPlaylist(self)
        path = QDir.current().absoluteFilePath('vida1.mov')
        self.playlist.addMedia(QMediaContent(QUrl.fromLocalFile(path)))
        self.playlist.setCurrentIndex(1)
        self.playlist.setPlaybackMode(QMediaPlaylist.CurrentItemInLoop)

        self.mediaPlayer1.setPlaylist(self.playlist)
        self.mediaPlayer1.setVideoOutput(self.drawer)

        self.playButton.toggled.connect(self.togglePlay)

    def togglePlay(self, play):
        if play:
            self.mediaPlayer1.play()
            self.playButton.setText('Pause')
        else:
            self.mediaPlayer1.pause()
            self.playButton.setText('Play')

import sys
app = QApplication(sys.argv)
test = AlphaVideoTest()
test.show()
sys.exit(app.exec_())

I based the above code on the following sources:

Note that I limited the supportedPixelFormats output, as using the full list of formats provided in the related question didn't work; this doesn't mean that this would work anyway, but that further testing is probably required, possibly on different machines and different OS/System configurations and video formats: remember that QMediaPlayer completely relies on the underlying OS and default media backend.

Finally, if you only need this for "limited" and predefined animations, consider implementing your own subclass of QWidget that uses a list of loaded PNG images and shows them by implementing paintEvent() that would be called by updates based on a QVariantAnimation. While this kind of implementation might result less performant or ideal, it has the major benefit of providing cross platform compatibility.

musicamante
  • 41,230
  • 6
  • 33
  • 58
  • wow. Can't thank you enough for your code and feedback. I've tried the code on mac, video runs but no alpha. Still black. I've tried adding, switching supportedPixelFormats outputs but no effect. This is a tough bone. – noi Jul 21 '21 at 23:06
  • @noi unfortunately mac is often a bit picky with these aspects. Do you get any output in the shell? – musicamante Jul 21 '21 at 23:13
  • I really like the QVariantAnimation approach. I don't need portability, so that's not an issue. I like the fact that I should not need to deal with the alpha frame by frame. I've found this: https://stackoverflow.com/questions/56247882/using-qvariantanimation-to-show-a-list-of-images Sounds like a good approach. The issue is to accomplish the 24fps. I don't know if it's possible to achieve a change of png every 42ms (x4, if there are 4 animations running). What do you think about that? – noi Jul 21 '21 at 23:14
  • @noi with a smart implementation, that could theoretically be achieved, but it depends on lots of aspects: using a QVariantAnimation with an *integer* value (with the value being the frame index), you can ensure that the animation is updated and time-synchronized no matter what (since it's based on the duration), the only issue is that the image updates don't "keep up", there would be some frame dropping depending on the computer capabilities. And, obviously, there could be some issues about memory usage, as it would probably load all frames in RAM unless proper implementation is used. – musicamante Jul 21 '21 at 23:21