aboutsummaryrefslogtreecommitdiff
path: root/voctocore/lib/quadmix.py
blob: 8d6930482e6800210db7b9ad70356c89ff919cdb (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. previewbins = []
  9. mixerpads = []
  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. def request_mixer_pad(self):
  30. previewbin = QuadMixPreview()
  31. self.add(previewbin)
  32. self.previewbins.append(previewbin)
  33. srcpad = previewbin.get_static_pad('src')
  34. sinkpad = previewbin.get_static_pad('sink')
  35. mixerpad = self.mixer.get_request_pad('sink_%u')
  36. self.mixerpads.append(mixerpad)
  37. srcpad.link(mixerpad)
  38. self.log.info('requested mixerpad %u (named %s)', len(self.mixerpads) - 1, mixerpad.get_name())
  39. ghostpad = Gst.GhostPad.new(mixerpad.get_name(), sinkpad)
  40. self.add_pad(ghostpad)
  41. return ghostpad
  42. def finalize(self):
  43. self.log.debug('all sources linked, calculating layout')
  44. # number of placed sources
  45. count = len(self.previewbins)
  46. # coordinate of the cell where we place the next video
  47. place = [0, 0]
  48. # number of cells in the quadmix-monitor
  49. grid = [0, 0]
  50. grid[0] = math.ceil(math.sqrt(count))
  51. grid[1] = math.ceil(count / grid[0])
  52. # size of each cell in the quadmix-monitor
  53. cellSize = (
  54. self.monitorSize[0] / grid[0],
  55. self.monitorSize[1] / grid[1]
  56. )
  57. # report calculation results
  58. 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',
  59. count, grid[0], grid[1], self.monitorSize[0], self.monitorSize[1], cellSize[0], cellSize[1])
  60. # iterate over all previewbins
  61. for idx, previewbin in enumerate(self.previewbins):
  62. # request srcpad to query videosize and aspect from it
  63. srcpad = previewbin.get_static_pad('src')
  64. # select the mixerpad responsible for this previewbin from the videomixer
  65. mixerpad = self.mixerpads[idx]
  66. # query the video-source caps and extract its size
  67. caps = srcpad.query_caps(None)
  68. capsstruct = caps.get_structure(0)
  69. srcSize = (
  70. capsstruct.get_int('width')[1],
  71. capsstruct.get_int('height')[1],
  72. )
  73. # calculate the ideal scale factor and scale the sizes
  74. f = max(srcSize[0] / cellSize[0], srcSize[1] / cellSize[1])
  75. scaleSize = (
  76. srcSize[0] / f,
  77. srcSize[1] / f,
  78. )
  79. # calculate the top/left coordinate
  80. coord = (
  81. place[0] * cellSize[0] + (cellSize[0] - scaleSize[0]) / 2,
  82. place[1] * cellSize[1] + (cellSize[1] - scaleSize[1]) / 2,
  83. )
  84. self.log.info('placing mixerpad %u of size %u×%u scaled by %u to %u×%u in a cell %u×%u px cell (%u/%u) at position (%u/%u)',
  85. idx, srcSize[0], srcSize[1], f, scaleSize[0], scaleSize[1], cellSize[0], cellSize[1], place[0], place[1], coord[0], coord[1])
  86. # request a pad from the quadmixer and configure x/y position
  87. mixerpad.set_property('xpos', round(coord[0]))
  88. mixerpad.set_property('ypos', round(coord[1]))
  89. previewbin.set_size(scaleSize)
  90. previewbin.set_idx(idx)
  91. # increment grid position
  92. place[0] += 1
  93. if place[0] >= grid[0]:
  94. place[1] += 1
  95. place[0] = 0
  96. def set_active(self, target):
  97. self.log.info('setting videosource %u active, disabling other', target)
  98. for idx, previewbin in enumerate(self.previewbins):
  99. previewbin.set_active(target == idx)
  100. class QuadMixPreview(Gst.Bin):
  101. log = logging.getLogger('QuadMixPreview')
  102. strokeWidth = 5
  103. def __init__(self):
  104. super().__init__()
  105. self.scale = Gst.ElementFactory.make('videoscale', 'scale')
  106. self.caps = Gst.ElementFactory.make('capsfilter', 'caps')
  107. self.cropbox = Gst.ElementFactory.make('videobox', 'cropbox')
  108. self.strokebox = Gst.ElementFactory.make('videobox', 'strokebox')
  109. self.textoverlay = Gst.ElementFactory.make('textoverlay', 'textoverlay')
  110. self.add(self.scale)
  111. self.add(self.caps)
  112. self.add(self.cropbox)
  113. self.add(self.strokebox)
  114. self.add(self.textoverlay)
  115. self.strokebox.set_property('fill', 'green')
  116. self.textoverlay.set_property('color', 0xFFFFFFFF)
  117. self.textoverlay.set_property('halignment', 'left')
  118. self.textoverlay.set_property('valignment', 'top')
  119. self.textoverlay.set_property('xpad', 10)
  120. self.textoverlay.set_property('ypad', 5)
  121. self.textoverlay.set_property('font-desc', 'sans 35')
  122. self.scale.link(self.caps)
  123. self.caps.link(self.cropbox)
  124. self.cropbox.link(self.strokebox)
  125. self.strokebox.link(self.textoverlay)
  126. self.set_active(False)
  127. # Add Ghost Pads
  128. self.add_pad(
  129. Gst.GhostPad.new('sink', self.scale.get_static_pad('sink'))
  130. )
  131. self.add_pad(
  132. Gst.GhostPad.new('src', self.textoverlay.get_static_pad('src'))
  133. )
  134. def set_size(self, scaleSize):
  135. caps = Gst.Caps.new_empty_simple('video/x-raw')
  136. caps.set_value('width', round(scaleSize[0]))
  137. caps.set_value('height', round(scaleSize[1]))
  138. self.caps.set_property('caps', caps)
  139. def set_idx(self, idx):
  140. self.textoverlay.set_property('text', str(idx))
  141. def set_active(self, active):
  142. self.log.info("switching active-state to %u", active)
  143. for side in ('top', 'left', 'right', 'bottom'):
  144. self.cropbox.set_property(side, self.strokeWidth if active else 0)
  145. self.strokebox.set_property(side, -self.strokeWidth if active else 0)
  146. def set_color(self, color):
  147. self.strokebox.set_property('fill', color)