aboutsummaryrefslogtreecommitdiff
path: root/voctocore/lib/quadmix.py
blob: 6a9384de6d41c328f60ae564ebe0882e1397bcf2 (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. # create a sub-preview-bin
  54. previewbin = QuadMixPreview()
  55. self.add(previewbin)
  56. self.previewbins.append(previewbin)
  57. previewsink = previewbin.get_static_pad('sink')
  58. previewsrc = previewbin.get_static_pad('src')
  59. srcpad = videosource.get_compatible_pad(previewsink, None)
  60. #srcpad.link(previewsink) # linking ghost pads
  61. print(videosource.link(previewbin))
  62. sinkpad = self.mixer.get_request_pad('sink_%u')
  63. #previewsrc.link(sinkpad) # linking ghost pads
  64. print(previewbin.link(self.mixer))
  65. # query the video-source caps and extract its size
  66. caps = srcpad.query_caps(None)
  67. capsstruct = caps.get_structure(0)
  68. srcSize = (
  69. capsstruct.get_int('width')[1],
  70. capsstruct.get_int('height')[1],
  71. )
  72. # calculate the ideal scale factor and scale the sizes
  73. f = max(srcSize[0] / cellSize[0], srcSize[1] / cellSize[1])
  74. scaleSize = (
  75. srcSize[0] / f,
  76. srcSize[1] / f,
  77. )
  78. # calculate the top/left coordinate
  79. coord = (
  80. place[0] * cellSize[0] + (cellSize[0] - scaleSize[0]) / 2,
  81. place[1] * cellSize[1] + (cellSize[1] - scaleSize[1]) / 2,
  82. )
  83. 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)',
  84. idx, srcSize[0], srcSize[1], f, scaleSize[0], scaleSize[1], cellSize[0], cellSize[1], place[0], place[1], coord[0], coord[1])
  85. # request a pad from the quadmixer and configure x/y position
  86. sinkpad.set_property('xpos', round(coord[0]))
  87. sinkpad.set_property('ypos', round(coord[1]))
  88. previewbin.set_size(scaleSize)
  89. previewbin.set_idx(idx)
  90. # increment grid position
  91. place[0] += 1
  92. if place[0] >= grid[0]:
  93. place[1] += 1
  94. place[0] = 0
  95. def set_active(self, target):
  96. for idx, previewbin in enumerate(self.previewbins):
  97. previewbin.set_active(target == idx)
  98. class QuadMixPreview(Gst.Bin):
  99. log = logging.getLogger('QuadMixPreview')
  100. strokeWidth = 5
  101. def __init__(self):
  102. super().__init__()
  103. self.scale = Gst.ElementFactory.make('videoscale', 'scale')
  104. self.caps = Gst.ElementFactory.make('capsfilter', 'caps')
  105. self.cropbox = Gst.ElementFactory.make('videobox', 'cropbox')
  106. self.strokebox = Gst.ElementFactory.make('videobox', 'strokebox')
  107. self.textoverlay = Gst.ElementFactory.make('textoverlay', 'textoverlay')
  108. self.add(self.scale)
  109. self.add(self.caps)
  110. self.add(self.cropbox)
  111. self.add(self.strokebox)
  112. self.add(self.textoverlay)
  113. self.strokebox.set_property('fill', 'green')
  114. self.textoverlay.set_property('color', 0xFFFFFFFF)
  115. self.textoverlay.set_property('halignment', 'left')
  116. self.textoverlay.set_property('valignment', 'top')
  117. self.textoverlay.set_property('xpad', 10)
  118. self.textoverlay.set_property('ypad', 5)
  119. self.textoverlay.set_property('font-desc', 'sans 35')
  120. self.scale.link(self.caps)
  121. self.caps.link(self.cropbox)
  122. self.cropbox.link(self.strokebox)
  123. self.strokebox.link(self.textoverlay)
  124. self.set_active(False)
  125. # Add Ghost Pads
  126. self.add_pad(
  127. Gst.GhostPad.new('sink', self.scale.get_static_pad('sink'))
  128. )
  129. self.add_pad(
  130. Gst.GhostPad.new('src', self.textoverlay.get_static_pad('src'))
  131. )
  132. def set_size(self, scaleSize):
  133. caps = Gst.Caps.new_empty_simple('video/x-raw')
  134. caps.set_value('width', round(scaleSize[0]))
  135. caps.set_value('height', round(scaleSize[1]))
  136. self.caps.set_property('caps', caps)
  137. def set_idx(self, idx):
  138. self.textoverlay.set_property('text', str(idx))
  139. def set_active(self, active):
  140. self.log.info("switching active-state to %u", active)
  141. for side in ('top', 'left', 'right', 'bottom'):
  142. self.cropbox.set_property(side, self.strokeWidth if active else 0)
  143. self.strokebox.set_property(side, -self.strokeWidth if active else 0)
  144. def set_color(self, color):
  145. self.strokebox.set_property('fill', color)