aboutsummaryrefslogtreecommitdiff
path: root/voctocore/lib/videomix.py
blob: 1bee90a7c559773404f2ba982c3ec92d2d8bae2e (plain)
  1. #!/usr/bin/python3
  2. import logging
  3. from gi.repository import Gst
  4. from enum import Enum
  5. from lib.config import Config
  6. class CompositeModes(Enum):
  7. fullscreen = 0
  8. side_by_side_equal = 1
  9. side_by_side_preview = 2
  10. picture_in_picture = 3
  11. class VideoMix(object):
  12. log = logging.getLogger('VideoMix')
  13. def __init__(self):
  14. self.caps = Config.get('mix', 'videocaps')
  15. self.names = Config.getlist('mix', 'sources')
  16. self.log.info('Configuring Mixer for %u Sources', len(self.names))
  17. pipeline = """
  18. videomixer name=mix !
  19. {caps} !
  20. queue !
  21. tee name=tee
  22. intervideosrc channel=mixer_background !
  23. {caps} !
  24. mix.
  25. tee. ! queue ! intervideosink channel=video_mix_out
  26. """.format(
  27. caps=self.caps
  28. )
  29. if Config.getboolean('previews', 'enabled'):
  30. pipeline += """
  31. tee. ! queue ! intervideosink channel=video_mix_preview
  32. """
  33. for idx, name in enumerate(self.names):
  34. pipeline += """
  35. intervideosrc channel=video_{name}_mixer !
  36. {caps} !
  37. videoscale !
  38. capsfilter name=caps_{idx} !
  39. mix.
  40. """.format(
  41. name=name,
  42. caps=self.caps,
  43. idx=idx
  44. )
  45. self.log.debug('Creating Mixing-Pipeline:\n%s', pipeline)
  46. self.mixingPipeline = Gst.parse_launch(pipeline)
  47. self.log.debug('Binding Error & End-of-Stream-Signal on Mixing-Pipeline')
  48. self.mixingPipeline.bus.add_signal_watch()
  49. self.mixingPipeline.bus.connect("message::eos", self.on_eos)
  50. self.mixingPipeline.bus.connect("message::error", self.on_error)
  51. self.log.debug('Initializing Mixer-State')
  52. self.compositeMode = CompositeModes.fullscreen
  53. self.sourceA = 0
  54. self.sourceB = 1
  55. self.updateMixerState()
  56. bgMixerpad = self.mixingPipeline.get_by_name('mix').get_static_pad('sink_0')
  57. bgMixerpad.set_property('zorder', 0)
  58. self.log.debug('Launching Mixing-Pipeline')
  59. self.mixingPipeline.set_state(Gst.State.PLAYING)
  60. def getInputVideoSize(self):
  61. caps = Gst.Caps.from_string(self.caps)
  62. struct = caps.get_structure(0)
  63. _, width = struct.get_int('width')
  64. _, height = struct.get_int('height')
  65. return width, height
  66. def updateMixerState(self):
  67. if self.compositeMode == CompositeModes.fullscreen:
  68. self.updateMixerStateFullscreen()
  69. elif self.compositeMode == CompositeModes.side_by_side_equal:
  70. self.updateMixerStateSideBySideEqual()
  71. elif self.compositeMode == CompositeModes.side_by_side_preview:
  72. self.updateMixerStateSideBySidePreview()
  73. elif self.compositeMode == CompositeModes.picture_in_picture:
  74. self.updateMixerStatePictureInPicture()
  75. def getMixerpadAndCapsfilter(self, idx):
  76. # mixerpad 0 = background
  77. mixerpad = self.mixingPipeline.get_by_name('mix').get_static_pad('sink_%u' % (idx+1))
  78. capsfilter = self.mixingPipeline.get_by_name('caps_%u' % idx)
  79. return mixerpad, capsfilter
  80. def updateMixerStateFullscreen(self):
  81. self.log.info('Updating Mixer-State for Fullscreen-Composition')
  82. noScaleCaps = Gst.Caps.from_string('video/x-raw')
  83. for idx, name in enumerate(self.names):
  84. alpha = int(idx == self.sourceA)
  85. mixerpad, capsfilter = self.getMixerpadAndCapsfilter(idx)
  86. self.log.debug('Setting Mixerpad %u to x/y=0 and alpha=%0.2f, zorder=%u', idx, alpha, 1)
  87. mixerpad.set_property('alpha', alpha)
  88. mixerpad.set_property('xpos', 0)
  89. mixerpad.set_property('ypos', 0)
  90. mixerpad.set_property('zorder', 1)
  91. self.log.debug('Resetting Scaler %u to non-scaling', idx)
  92. capsfilter.set_property('caps', noScaleCaps)
  93. def updateMixerStateSideBySideEqual(self):
  94. self.log.info('Updating Mixer-State for Side-by-side-Equal-Composition')
  95. width, height = self.getInputVideoSize()
  96. self.log.debug('Video-Size parsed as %ux%u', width, height)
  97. try:
  98. gutter = Config.getint('side-by-side-equal', 'gutter')
  99. self.log.debug('Gutter configured to %u', gutter)
  100. except:
  101. gutter = int(width / 100)
  102. self.log.debug('Gutter calculated to %u', gutter)
  103. targetWidth = int((width - gutter) / 2)
  104. targetHeight = int(targetWidth / width * height)
  105. y = (height - targetHeight) / 2
  106. xa = 0
  107. xb = width - targetWidth
  108. scaleCaps = Gst.Caps.from_string('video/x-raw,width=%u,height=%u' % (targetWidth, targetHeight))
  109. noScaleCaps = Gst.Caps.from_string('video/x-raw')
  110. for idx, name in enumerate(self.names):
  111. mixerpad, capsfilter = self.getMixerpadAndCapsfilter(idx)
  112. if idx == self.sourceA:
  113. mixerpad.set_property('alpha', 1)
  114. mixerpad.set_property('xpos', xa)
  115. mixerpad.set_property('ypos', y)
  116. mixerpad.set_property('zorder', 1)
  117. capsfilter.set_property('caps', scaleCaps)
  118. self.log.debug('Setting Mixerpad %u to x/y=%u/%u and alpha=%0.2f, zorder=%u', idx, xa, y, 1, 1)
  119. self.log.debug('Setting Scaler %u to %u/%u', idx, targetWidth, targetHeight)
  120. elif idx == self.sourceB:
  121. mixerpad.set_property('alpha', 1)
  122. mixerpad.set_property('xpos', xb)
  123. mixerpad.set_property('ypos', y)
  124. mixerpad.set_property('zorder', 1)
  125. capsfilter.set_property('caps', scaleCaps)
  126. self.log.debug('Setting Mixerpad %u to x/y=%u/%u and alpha=%0.2f, zorder=%u', idx, xb, y, 1, 1)
  127. self.log.debug('Setting Scaler %u to %u/%u', idx, targetWidth, targetHeight)
  128. else:
  129. mixerpad.set_property('alpha', 0)
  130. mixerpad.set_property('xpos', 0)
  131. mixerpad.set_property('ypos', 0)
  132. capsfilter.set_property('caps', noScaleCaps)
  133. self.log.debug('Setting Mixerpad %u to x/y=%u/%u and alpha=%0.2f', idx, 0, 0, 0)
  134. self.log.debug('Resetting Scaler %u to non-scaling', idx)
  135. def updateMixerStateSideBySidePreview(self):
  136. self.log.info('Updating Mixer-State for Side-by-side-Preview-Composition')
  137. width, height = self.getInputVideoSize()
  138. self.log.debug('Video-Size parsed as %ux%u', width, height)
  139. try:
  140. asize = [int(i) for i in Config.get('side-by-side-preview', 'asize').split('x', 1)]
  141. self.log.debug('A-Video-Size configured to %ux%u', asize[0], asize[1])
  142. except:
  143. asize = [
  144. int(width / 1.25), # 80%
  145. int(height / 1.25) # 80%
  146. ]
  147. self.log.debug('A-Video-Size calculated to %ux%u', asize[0], asize[1])
  148. try:
  149. apos = [int(i) for i in Config.get('side-by-side-preview', 'apos').split('/', 1)]
  150. self.log.debug('B-Video-Position configured to %u/%u', apos[0], apos[1])
  151. except:
  152. apos = [
  153. int(width / 100), # 1%
  154. int(width / 100) # 1%
  155. ]
  156. self.log.debug('B-Video-Position calculated to %u/%u', apos[0], apos[1])
  157. try:
  158. bsize = [int(i) for i in Config.get('side-by-side-preview', 'bsize').split('x', 1)]
  159. self.log.debug('B-Video-Size configured to %ux%u', bsize[0], bsize[1])
  160. except:
  161. bsize = [
  162. int(width / 4), # 25%
  163. int(height / 4) # 25%
  164. ]
  165. self.log.debug('B-Video-Size calculated to %ux%u', bsize[0], bsize[1])
  166. try:
  167. bpos = [int(i) for i in Config.get('side-by-side-preview', 'bpos').split('/', 1)]
  168. self.log.debug('B-Video-Position configured to %u/%u', bpos[0], bpos[1])
  169. except:
  170. bpos = [
  171. width - int(width / 100) - bsize[0],
  172. height - int(width / 100) - bsize[1] # 1%
  173. ]
  174. self.log.debug('B-Video-Position calculated to %u/%u', bpos[0], bpos[1])
  175. aCaps = Gst.Caps.from_string('video/x-raw,width=%u,height=%u' % tuple(asize))
  176. bCaps = Gst.Caps.from_string('video/x-raw,width=%u,height=%u' % tuple(bsize))
  177. noScaleCaps = Gst.Caps.from_string('video/x-raw')
  178. for idx, name in enumerate(self.names):
  179. mixerpad, capsfilter = self.getMixerpadAndCapsfilter(idx)
  180. if idx == self.sourceA:
  181. mixerpad.set_property('alpha', 1)
  182. mixerpad.set_property('xpos', apos[1])
  183. mixerpad.set_property('ypos', apos[1])
  184. mixerpad.set_property('zorder', 1)
  185. capsfilter.set_property('caps', aCaps)
  186. self.log.debug('Setting Mixerpad %u to x/y=%u/%u and alpha=%0.2f, zorder=%u', idx, apos[0], apos[1], 1, 1)
  187. self.log.debug('Setting Scaler %u to %u/%u', idx, asize[0], asize[1])
  188. elif idx == self.sourceB:
  189. mixerpad.set_property('alpha', 1)
  190. mixerpad.set_property('xpos', bpos[0])
  191. mixerpad.set_property('ypos', bpos[1])
  192. mixerpad.set_property('zorder', 2)
  193. capsfilter.set_property('caps', bCaps)
  194. self.log.debug('Setting Mixerpad %u to x/y=%u/%u, alpha=%0.2f, zorder=%u', idx, bpos[0], bpos[1], 1, 2)
  195. self.log.debug('Setting Scaler %u to %u/%u', idx, bsize[0], bsize[1])
  196. else:
  197. mixerpad.set_property('alpha', 0)
  198. mixerpad.set_property('xpos', 0)
  199. mixerpad.set_property('ypos', 0)
  200. capsfilter.set_property('caps', noScaleCaps)
  201. self.log.debug('Setting Mixerpad %u to x/y=%u/%u and alpha=%0.2f', idx, 0, 0, 0)
  202. self.log.debug('Resetting Scaler %u to non-scaling', idx)
  203. def updateMixerStatePictureInPicture(self):
  204. self.log.info('Updating Mixer-State for Picture-in-Picture-Composition')
  205. width, height = self.getInputVideoSize()
  206. self.log.debug('Video-Size parsed as %ux%u', width, height)
  207. try:
  208. pipsize = [int(i) for i in Config.get('picture-in-picture', 'pipsize').split('x', 1)]
  209. self.log.debug('PIP-Size configured to %ux%u', pipsize[0], pipsize[1])
  210. except:
  211. pipsize = [
  212. int(width / 4), # 25%
  213. int(height / 4) # 25%
  214. ]
  215. self.log.debug('PIP-Size calculated to %ux%u', pipsize[0], pipsize[1])
  216. try:
  217. pippos = [int(i) for i in Config.get('picture-in-picture', 'pippos').split('/', 1)]
  218. self.log.debug('PIP-Position configured to %u/%u', pippos[0], pippos[1])
  219. except:
  220. pippos = [
  221. width - pipsize[0] - int(width / 100), # 1%
  222. height - pipsize[1] -int(width / 100) # 1%
  223. ]
  224. self.log.debug('PIP-Position calculated to %u/%u', pippos[0], pippos[1])
  225. scaleCaps = Gst.Caps.from_string('video/x-raw,width=%u,height=%u' % tuple(pipsize))
  226. noScaleCaps = Gst.Caps.from_string('video/x-raw')
  227. for idx, name in enumerate(self.names):
  228. mixerpad, capsfilter = self.getMixerpadAndCapsfilter(idx)
  229. if idx == self.sourceA:
  230. mixerpad.set_property('alpha', 1)
  231. mixerpad.set_property('xpos', 0)
  232. mixerpad.set_property('ypos', 0)
  233. mixerpad.set_property('zorder', 1)
  234. capsfilter.set_property('caps', noScaleCaps)
  235. self.log.debug('Setting Mixerpad %u to x/y=%u/%u and alpha=%0.2f, zorder=%u', idx, 0, 0, 1, 1)
  236. self.log.debug('Resetting Scaler %u to non-scaling', idx)
  237. elif idx == self.sourceB:
  238. mixerpad.set_property('alpha', 1)
  239. mixerpad.set_property('xpos', pippos[0])
  240. mixerpad.set_property('ypos', pippos[1])
  241. mixerpad.set_property('zorder', 2)
  242. capsfilter.set_property('caps', scaleCaps)
  243. self.log.debug('Setting Mixerpad %u to x/y=%u/%u, alpha=%0.2f, zorder=%u', idx, pippos[0], pippos[1], 1, 2)
  244. self.log.debug('Setting Scaler %u to %u/%u', idx, pipsize[0], pipsize[1])
  245. else:
  246. mixerpad.set_property('alpha', 0)
  247. mixerpad.set_property('xpos', 0)
  248. mixerpad.set_property('ypos', 0)
  249. capsfilter.set_property('caps', noScaleCaps)
  250. self.log.debug('Setting Mixerpad %u to x/y=%u/%u and alpha=%0.2f', idx, 0, 0, 0)
  251. self.log.debug('Resetting Scaler %u to non-scaling', idx)
  252. def setVideoSourceA(self, source):
  253. # swap if required
  254. if self.sourceB == source:
  255. self.sourceB = self.sourceA
  256. self.sourceA = source
  257. self.updateMixerState()
  258. def setVideoSourceB(self, source):
  259. # swap if required
  260. if self.sourceA == source:
  261. self.sourceA = self.sourceB
  262. self.sourceB = source
  263. self.updateMixerState()
  264. def setCompositeMode(self, mode):
  265. self.compositeMode = mode
  266. self.updateMixerState()
  267. def on_eos(self, bus, message):
  268. self.log.debug('Received End-of-Stream-Signal on Mixing-Pipeline')
  269. def on_error(self, bus, message):
  270. self.log.debug('Received Error-Signal on Mixing-Pipeline')
  271. (error, debug) = message.parse_error()
  272. self.log.debug('Error-Details: #%u: %s', error.code, debug)