video.py 9.39 KB
Newer Older
Dan Pascu's avatar
Dan Pascu committed
1

Adrian Georgescu's avatar
Adrian Georgescu committed
2

Dan Pascu's avatar
Dan Pascu committed
3

Dan Pascu's avatar
Dan Pascu committed
4 5 6
from PyQt5.QtCore import Qt, QMetaObject, QPoint, QRect, QTimer, pyqtSignal
from PyQt5.QtGui import QColor, QCursor, QIcon, QImage, QPainter, QPixmap, QTransform
from PyQt5.QtWidgets import QWidget
Dan Pascu's avatar
Dan Pascu committed
7 8 9 10 11 12 13 14 15

from application.python.types import MarkerType
from math import ceil
from operator import truediv
from sipsimple.core import FrameBufferVideoRenderer

from blink.resources import Resources


16 17 18 19 20
__all__ = ['VideoSurface']


class Container(object):
    pass
Dan Pascu's avatar
Dan Pascu committed
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39


class InteractionState(object):
    def __init__(self):
        self.moving = False
        self.resizing = False
        self.resize_corner = None
        self.mouse_last_position = None
        self.initial_geometry = None

    @property
    def active(self):
        return self.moving or self.resizing

    def clear(self):
        self.__init__()


class VideoSurface(QWidget):
Adrian Georgescu's avatar
Adrian Georgescu committed
40 41 42 43
    class TopLeftCorner(metaclass=MarkerType):     pass
    class TopRightCorner(metaclass=MarkerType):    pass
    class BottomLeftCorner(metaclass=MarkerType):  pass
    class BottomRightCorner(metaclass=MarkerType): pass
Dan Pascu's avatar
Dan Pascu committed
44

45
    adjusted = pyqtSignal(QRect, QRect)  # the widget was adjusted by the user (if interactive)
Dan Pascu's avatar
Dan Pascu committed
46 47 48 49 50 51 52 53

    interactive = False  # if the widget can be interacted with (moved, resized)
    mirror = False       # mirror the image horizontally

    def __init__(self, parent=None, framerate=None):
        super(VideoSurface, self).__init__(parent)
        self.setAttribute(Qt.WA_OpaquePaintEvent, True)
        self.setMouseTracking(True)
54
        self.cursors = Container()
Dan Pascu's avatar
Dan Pascu committed
55 56 57 58
        self.cursors.resize_top    = QCursor(QIcon(Resources.get('icons/resize-top.svg')).pixmap(16),    hotX=8,  hotY=0)
        self.cursors.resize_bottom = QCursor(QIcon(Resources.get('icons/resize-bottom.svg')).pixmap(16), hotX=8,  hotY=16)
        if framerate is not None:
            self._clock = QTimer()
59
            self._clock.setInterval(int(1000/framerate))
Dan Pascu's avatar
Dan Pascu committed
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
            self._clock.timeout.connect(self.update)
        else:
            self._clock = None
        self._interaction = InteractionState()
        self._image = None

    def __getattr__(self, name):
        if name == '_renderer':
            return self.__dict__.setdefault(name, FrameBufferVideoRenderer(self._handle_frame))
        raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, name))

    def _get_producer(self):
        return self._renderer.producer

    def _set_producer(self, producer):
        if self._clock is not None:
            self._clock.stop()
        self._renderer.producer = producer
        if self._clock is not None and producer is not None:
            self._clock.start()

    producer = property(_get_producer, _set_producer)
    del _get_producer, _set_producer

    @property
    def aspect(self):
        producer = self._renderer.producer
        return truediv(*producer.framesize) if producer is not None else 16/9

    def width_for_height(self, height):
        return int(ceil(height * self.aspect))

    def height_for_width(self, width):
        return int(ceil(width / self.aspect))

    def heightForWidth(self, width):
        return int(ceil(width * 9/16))

    def stop(self):
        if self._clock is not None:
            self._clock.stop()
        self._renderer.close()
        del self._renderer

    def _handle_frame(self, frame):
        self._image = QImage(frame.data, frame.width, frame.height, QImage.Format_ARGB32)
        if self._clock is None:
            QMetaObject.invokeMethod(self, 'update', Qt.QueuedConnection)

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.fillRect(self.rect(), QColor('#101010'))

        image = self._image

        if image is not None:
            if self.height() < 240:
                fast_scaler = QTransform()
                scale = 297/image.height()
                if self.mirror:
                    fast_scaler.scale(-scale, scale)
                else:
                    fast_scaler.scale(scale, scale)
                rect = event.rect()
124
                painter.drawPixmap(rect, QPixmap.fromImage(image.transformed(fast_scaler)).scaledToHeight(self.height(), Qt.SmoothTransformation), rect)
Dan Pascu's avatar
Dan Pascu committed
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
            else:
                transform = QTransform()
                scale = min(self.width()/image.width(), self.height()/image.height())
                if self.mirror:
                    transform.translate((self.width() + image.width()*scale)/2, (self.height() - image.height()*scale)/2)
                    transform.scale(-scale, scale)
                else:
                    transform.translate((self.width() - image.width()*scale)/2, (self.height() - image.height()*scale)/2)
                    transform.scale(scale, scale)

                inverse_transform, invertible = transform.inverted()
                rect = inverse_transform.mapRect(event.rect()).adjusted(-1, -1, 1, 1).intersected(image.rect())

                painter.setTransform(transform)

                if self.height() > 400:
                    painter.drawPixmap(rect, QPixmap.fromImage(image), rect)
                else:
                    painter.drawImage(rect, image, rect)

        painter.end()

    def mousePressEvent(self, event):
        if self.interactive and event.button() == Qt.LeftButton and event.modifiers() == Qt.NoModifier:
            if self.rect().adjusted(0, 10, 0, -10).contains(event.pos()):
                self._interaction.moving = True
            else:
                self._interaction.resizing = True
                event_x = event.x()
                event_y = event.y()
                half_width = self.width() / 2
                half_height = self.height() / 2
                if event_x < half_width and event_y < half_height:
                    self._interaction.resize_corner = self.TopLeftCorner
                elif event_x >= half_width and event_y < half_height:
                    self._interaction.resize_corner = self.TopRightCorner
                elif event_x < half_width and event_y >= half_height:
                    self._interaction.resize_corner = self.BottomLeftCorner
                else:
                    self._interaction.resize_corner = self.BottomRightCorner
            self._interaction.mouse_last_position = event.globalPos()
            self._interaction.initial_geometry = self.geometry()

    def mouseReleaseEvent(self, event):
        if self._interaction.active and self._interaction.initial_geometry != self.geometry():
            self.adjusted.emit(self._interaction.initial_geometry, self.geometry())
        self._interaction.clear()

    def mouseMoveEvent(self, event):
        if self._interaction.moving:
            mouse_position = event.globalPos()
            offset = mouse_position - self._interaction.mouse_last_position
            if self.parent() is not None:
                parent_rect = self.parent().rect()
                old_geometry = self.geometry()
                new_geometry = old_geometry.translated(offset)
                if new_geometry.left() < 0:
                    new_geometry.moveLeft(0)
                if new_geometry.top() < 0:
                    new_geometry.moveTop(0)
                if new_geometry.right() > parent_rect.right():
                    new_geometry.moveRight(parent_rect.right())
                if new_geometry.bottom() > parent_rect.bottom():
                    new_geometry.moveBottom(parent_rect.bottom())
                offset = new_geometry.topLeft() - old_geometry.topLeft()
            self.move(self.pos() + offset)
            self._interaction.mouse_last_position += offset
        elif self._interaction.resizing:
            mouse_position = event.globalPos()
            delta_y = mouse_position.y() - self._interaction.mouse_last_position.y()
            geometry = self.geometry()

            if self._interaction.resize_corner is self.TopLeftCorner:
                delta_x = -(self.width_for_height(geometry.height() - delta_y) - geometry.width())
                geometry.setTopLeft(geometry.topLeft() + QPoint(delta_x, delta_y))
            elif self._interaction.resize_corner is self.TopRightCorner:
201
                delta_x = +(self.width_for_height(geometry.height() - delta_y) - geometry.width())
Dan Pascu's avatar
Dan Pascu committed
202 203 204 205 206
                geometry.setTopRight(geometry.topRight() + QPoint(delta_x, delta_y))
            elif self._interaction.resize_corner is self.BottomLeftCorner:
                delta_x = -(self.width_for_height(geometry.height() + delta_y) - geometry.width())
                geometry.setBottomLeft(geometry.bottomLeft() + QPoint(delta_x, delta_y))
            else:
207
                delta_x = +(self.width_for_height(geometry.height() + delta_y) - geometry.width())
Dan Pascu's avatar
Dan Pascu committed
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
                geometry.setBottomRight(geometry.bottomRight() + QPoint(delta_x, delta_y))

            if self.minimumHeight() <= geometry.height() <= self.maximumHeight() and (self.parent() is None or self.parent().rect().contains(geometry)):
                self.setGeometry(geometry)
                self._interaction.mouse_last_position = mouse_position
        elif self.interactive:
            mouse_position = event.pos()
            topbar_rect = QRect(0, 0, self.width(), 10)

            if self.rect().adjusted(0, 10, 0, -10).contains(mouse_position):
                self.setCursor(Qt.ArrowCursor)
            elif topbar_rect.contains(mouse_position):
                self.setCursor(self.cursors.resize_top)
            else:
                self.setCursor(self.cursors.resize_bottom)

    def closeEvent(self, event):
        super(VideoSurface, self).closeEvent(event)
        self.stop()