aboutsummaryrefslogtreecommitdiff
path: root/voctocore/lib/quadmix.py
blob: 51159190443873f925a456f56c54bcb9809c0144 (plain)
  1. #!/usr/bin/python3
  2. import math, logging
  3. from gi.repository import GLib, Gst
  4. from lib.helper import iteratorHelper
  5. from lib.config import Config
  6. class QuadMix(Gst.Bin):
  7. log = logging.getLogger('QuadMix')
  8. sources = []
  9. previewbins = []
  10. def __init__(self):
  11. super().__init__()
  12. caps = Gst.Caps.from_string(Config.get('mix', 'monitorcaps'))
  13. self.log.debug('parsing monitorcaps from config: %s', caps.to_string())
  14. struct = caps.get_structure(0)
  15. self.monitorSize = [struct.get_int('width')[1], struct.get_int('height')[1]]
  16. self.bgsrc = Gst.ElementFactory.make('videotestsrc', 'bgsrc')
  17. self.mixer = Gst.ElementFactory.make('videomixer', 'mixer')
  18. self.scale = Gst.ElementFactory.make('videoscale', 'scale')
  19. self.add(self.bgsrc)
  20. self.add(self.mixer)
  21. self.add(self.scale)
  22. self.bgsrc.link_filtered(self.mixer, caps)
  23. self.mixer.link_filtered(self.scale, caps)
  24. self.bgsrc.set_property('pattern', 'solid-color')
  25. self.bgsrc.set_property('foreground-color', 0x808080)
  26. self.add_pad(
  27. Gst.GhostPad.new('src', self.scale.get_static_pad('src'))
  28. )
  29. # I don't know how to create a on-request ghost-pad
  30. def add_source(self, src):
  31. self.log.info('adding source %s', src.get_name())
  32. self.sources.append(src)
  33. def finalize(self):
  34. self.log.debug('all sources added, calculating layout')
  35. # number of placed sources
  36. count = len(self.sources)
  37. # coordinate of the cell where we place the next video
  38. place = [0, 0]
  39. # number of cells in the quadmix-monitor
  40. grid = [0, 0]
  41. grid[0] = math.ceil(math.sqrt(count))
  42. grid[1] = math.ceil(count / grid[0])
  43. # size of each cell in the quadmix-monitor
  44. cellSize = (
  45. self.monitorSize[0] / grid[0],
  46. self.monitorSize[1] / grid[1]
  47. )
  48. # report calculation results
  49. self.log.info('showing %u videosources in a %u×%u grid in a %u×%u px window, which gives cells of %u×%u px per videosource',
  50. count, grid[0], grid[1], self.monitorSize[0], self.monitorSize[1], cellSize[0], cellSize[1])
  51. # iterate over all video-sources
  52. for idx, videosource in enumerate(self.sources):
  53. # query the video-source caps and extract its size
  54. caps = videosource.get_static_pad('src').query_caps(None)
  55. capsstruct = caps.get_structure(0)
  56. srcSize = (
  57. capsstruct.get_int('width')[1],
  58. capsstruct.get_int('height')[1],
  59. )
  60. # calculate the ideal scale factor and scale the sizes
  61. f = max(srcSize[0] / cellSize[0], srcSize[1] / cellSize[1])
  62. scaleSize = (
  63. srcSize[0] / f,
  64. srcSize[1] / f,
  65. )
  66. # calculate the top/left coordinate
  67. coord = (
  68. place[0] * cellSize[0] + (cellSize[0] - scaleSize[0]) / 2,
  69. place[1] * cellSize[1] + (cellSize[1] - scaleSize[1]) / 2,
  70. )
  71. self.log.info('placing videosource %u of size %u×%u scaled by %u to %u×%u in a cell %u×%u px cell (%u/%u) at position (%u/%u)',
  72. idx, srcSize[0], srcSize[1], f, scaleSize[0], scaleSize[1], cellSize[0], cellSize[1], place[0], place[1], coord[0], coord[1])
  73. # request a pad from the quadmixer and configure x/y position
  74. sinkpad = self.mixer.get_request_pad('sink_%u')
  75. sinkpad.set_property('xpos', round(coord[0]))
  76. sinkpad.set_property('ypos', round(coord[1]))
  77. # create a sub-preview-bin
  78. previewbin = QuadMixPreview(idx, scaleSize)
  79. self.add(previewbin)
  80. self.previewbins.append(previewbin)
  81. # link videosource to input of previewbin
  82. videosource.link(previewbin)
  83. # link the output of the preview-bin to the mixer
  84. previewbin.get_static_pad('src').link(sinkpad)
  85. # increment grid position
  86. place[0] += 1
  87. if place[0] >= grid[0]:
  88. place[1] += 1
  89. place[0] = 0
  90. def set_active(self, target):
  91. for idx, previewbin in enumerate(self.previewbins):
  92. previewbin.set_active(target == idx)
  93. class QuadMixPreview(Gst.Bin):
  94. log = logging.getLogger('QuadMixPreview')
  95. strokeWidth = 5
  96. def __init__(self, idx, scaleSize):
  97. super().__init__()
  98. self.scale = Gst.ElementFactory.make('videoscale', 'scale')
  99. self.cropbox = Gst.ElementFactory.make('videobox', 'cropbox')
  100. self.strokebox = Gst.ElementFactory.make('videobox', 'strokebox')
  101. self.textoverlay = Gst.ElementFactory.make('textoverlay', 'textoverlay')
  102. self.add(self.scale)
  103. self.add(self.cropbox)
  104. self.add(self.strokebox)
  105. self.add(self.textoverlay)
  106. caps = Gst.Caps.new_empty_simple('video/x-raw')
  107. caps.set_value('width', round(scaleSize[0]))
  108. caps.set_value('height', round(scaleSize[1]))
  109. self.strokebox.set_property('fill', 'green')
  110. self.textoverlay.set_property('color', 0xFFFFFFFF)
  111. self.textoverlay.set_property('halignment', 'left')
  112. self.textoverlay.set_property('valignment', 'top')
  113. self.textoverlay.set_property('xpad', 10)
  114. self.textoverlay.set_property('ypad', 5)
  115. self.textoverlay.set_property('font-desc', 'sans 35')
  116. self.scale.link_filtered(self.cropbox, caps)
  117. self.cropbox.link(self.strokebox)
  118. self.strokebox.link(self.textoverlay)
  119. self.set_active(False)
  120. # Add Ghost Pads
  121. self.add_pad(
  122. Gst.GhostPad.new('sink', self.scale.get_static_pad('sink'))
  123. )
  124. self.add_pad(
  125. Gst.GhostPad.new('src', self.textoverlay.get_static_pad('src'))
  126. )
  127. def set_active(self, active):
  128. self.log.info("switching active-state to %u", active)
  129. for side in ('top', 'left', 'right', 'bottom'):
  130. self.cropbox.set_property(side, self.strokeWidth if active else 0)
  131. self.strokebox.set_property(side, -self.strokeWidth if active else 0)
  132. def setColor(self, color):
  133. self.strokebox.set_property('fill', color)