aboutsummaryrefslogtreecommitdiff
path: root/voctocore/lib/videomix.py
blob: e7c02331ca2248d0a9e67e793866a5d61f2220ee (plain)
  1. import logging
  2. from gi.repository import Gst
  3. from enum import Enum, unique
  4. from lib.config import Config
  5. from lib.clock import Clock
  6. @unique
  7. class CompositeModes(Enum):
  8. fullscreen = 0
  9. side_by_side_equal = 1
  10. side_by_side_preview = 2
  11. picture_in_picture = 3
  12. matrix_two_by_two = 4
  13. class PadState(object):
  14. def __init__(self):
  15. self.reset()
  16. def reset(self):
  17. self.alpha = 1.0
  18. self.xpos = 0
  19. self.ypos = 0
  20. self.zorder = 1
  21. self.width = 0
  22. self.height = 0
  23. class VideoMix(object):
  24. log = logging.getLogger('VideoMix')
  25. def __init__(self):
  26. self.caps = Config.get('mix', 'videocaps')
  27. self.names = Config.getlist('mix', 'sources')
  28. self.log.info('Configuring Mixer for %u Sources', len(self.names))
  29. pipeline = """
  30. compositor name=mix !
  31. {caps} !
  32. identity name=sig !
  33. queue !
  34. tee name=tee
  35. intervideosrc channel=video_background !
  36. {caps} !
  37. mix.
  38. tee. ! queue ! intervideosink channel=video_mix_out
  39. """.format(
  40. caps=self.caps
  41. )
  42. if Config.getboolean('previews', 'enabled'):
  43. pipeline += """
  44. tee. ! queue ! intervideosink channel=video_mix_preview
  45. """
  46. if Config.getboolean('stream-blanker', 'enabled'):
  47. pipeline += """
  48. tee. ! queue ! intervideosink channel=video_mix_streamblanker
  49. """
  50. for idx, name in enumerate(self.names):
  51. pipeline += """
  52. intervideosrc channel=video_{name}_mixer !
  53. {caps} !
  54. mix.
  55. """.format(
  56. name=name,
  57. caps=self.caps,
  58. idx=idx
  59. )
  60. self.log.debug('Creating Mixing-Pipeline:\n%s', pipeline)
  61. self.mixingPipeline = Gst.parse_launch(pipeline)
  62. self.mixingPipeline.use_clock(Clock)
  63. self.log.debug('Binding Error & End-of-Stream-Signal '
  64. 'on Mixing-Pipeline')
  65. self.mixingPipeline.bus.add_signal_watch()
  66. self.mixingPipeline.bus.connect("message::eos", self.on_eos)
  67. self.mixingPipeline.bus.connect("message::error", self.on_error)
  68. self.log.debug('Binding Handoff-Handler for '
  69. 'Synchronus mixer manipulation')
  70. sig = self.mixingPipeline.get_by_name('sig')
  71. sig.connect('handoff', self.on_handoff)
  72. self.padStateDirty = False
  73. self.padState = list()
  74. for idx, name in enumerate(self.names):
  75. self.padState.append(PadState())
  76. self.log.debug('Initializing Mixer-State')
  77. self.compositeMode = CompositeModes.fullscreen
  78. self.sourceA = 0
  79. self.sourceB = 1
  80. self.sourceC = 2
  81. self.sourceD = 3
  82. self.recalculateMixerState()
  83. self.applyMixerState()
  84. bgMixerpad = (self.mixingPipeline.get_by_name('mix')
  85. .get_static_pad('sink_0'))
  86. bgMixerpad.set_property('zorder', 0)
  87. self.log.debug('Launching Mixing-Pipeline')
  88. self.mixingPipeline.set_state(Gst.State.PLAYING)
  89. def getInputVideoSize(self):
  90. caps = Gst.Caps.from_string(self.caps)
  91. struct = caps.get_structure(0)
  92. _, width = struct.get_int('width')
  93. _, height = struct.get_int('height')
  94. return width, height
  95. def recalculateMixerState(self):
  96. if self.compositeMode == CompositeModes.fullscreen:
  97. self.recalculateMixerStateFullscreen()
  98. elif self.compositeMode == CompositeModes.side_by_side_equal:
  99. self.recalculateMixerStateSideBySideEqual()
  100. elif self.compositeMode == CompositeModes.side_by_side_preview:
  101. self.recalculateMixerStateSideBySidePreview()
  102. elif self.compositeMode == CompositeModes.picture_in_picture:
  103. self.recalculateMixerStatePictureInPicture()
  104. elif self.compositeMode == CompositeModes.matrix_two_by_two:
  105. self.recalculateMixerStateMatrixTwoByTwo()
  106. self.log.debug('Marking Pad-State as Dirty')
  107. self.padStateDirty = True
  108. def recalculateMixerStateFullscreen(self):
  109. self.log.info('Updating Mixer-State for Fullscreen-Composition')
  110. for idx, name in enumerate(self.names):
  111. pad = self.padState[idx]
  112. pad.reset()
  113. pad.alpha = float(idx == self.sourceA)
  114. def recalculateMixerStateSideBySideEqual(self):
  115. self.log.info('Updating Mixer-State for '
  116. 'Side-by-side-Equal-Composition')
  117. width, height = self.getInputVideoSize()
  118. self.log.debug('Video-Size parsed as %ux%u', width, height)
  119. try:
  120. gutter = Config.getint('side-by-side-equal', 'gutter')
  121. self.log.debug('Gutter configured to %u', gutter)
  122. except:
  123. gutter = int(width / 100)
  124. self.log.debug('Gutter calculated to %u', gutter)
  125. targetWidth = int((width - gutter) / 2)
  126. targetHeight = int(targetWidth / width * height)
  127. self.log.debug('Video-Size calculated to %ux%u',
  128. targetWidth, targetHeight)
  129. xa = 0
  130. xb = width - targetWidth
  131. y = (height - targetHeight) / 2
  132. try:
  133. ya = Config.getint('side-by-side-equal', 'atop')
  134. self.log.debug('A-Video Y-Pos configured to %u', ya)
  135. except:
  136. ya = y
  137. self.log.debug('A-Video Y-Pos calculated to %u', ya)
  138. try:
  139. yb = Config.getint('side-by-side-equal', 'btop')
  140. self.log.debug('B-Video Y-Pos configured to %u', yb)
  141. except:
  142. yb = y
  143. self.log.debug('B-Video Y-Pos calculated to %u', yb)
  144. for idx, name in enumerate(self.names):
  145. pad = self.padState[idx]
  146. pad.reset()
  147. pad.width = targetWidth
  148. pad.height = targetHeight
  149. if idx == self.sourceA:
  150. pad.xpos = xa
  151. pad.ypos = ya
  152. pad.zorder = 1
  153. elif idx == self.sourceB:
  154. pad.xpos = xb
  155. pad.ypos = yb
  156. pad.zorder = 2
  157. else:
  158. pad.alpha = 0
  159. def recalculateMixerStateSideBySidePreview(self):
  160. self.log.info('Updating Mixer-State for '
  161. 'Side-by-side-Preview-Composition')
  162. width, height = self.getInputVideoSize()
  163. self.log.debug('Video-Size parsed as %ux%u', width, height)
  164. try:
  165. asize = [int(i) for i in Config.get('side-by-side-preview',
  166. 'asize').split('x', 1)]
  167. self.log.debug('A-Video-Size configured to %ux%u',
  168. asize[0], asize[1])
  169. except:
  170. asize = [
  171. int(width / 1.25), # 80%
  172. int(height / 1.25) # 80%
  173. ]
  174. self.log.debug('A-Video-Size calculated to %ux%u',
  175. asize[0], asize[1])
  176. try:
  177. apos = [int(i) for i in Config.get('side-by-side-preview',
  178. 'apos').split('/', 1)]
  179. self.log.debug('A-Video-Position configured to %u/%u',
  180. apos[0], apos[1])
  181. except:
  182. apos = [
  183. int(width / 100), # 1%
  184. int(width / 100) # 1%
  185. ]
  186. self.log.debug('A-Video-Position calculated to %u/%u',
  187. apos[0], apos[1])
  188. try:
  189. bsize = [int(i) for i in Config.get('side-by-side-preview',
  190. 'bsize').split('x', 1)]
  191. self.log.debug('B-Video-Size configured to %ux%u',
  192. bsize[0], bsize[1])
  193. except:
  194. bsize = [
  195. int(width / 4), # 25%
  196. int(height / 4) # 25%
  197. ]
  198. self.log.debug('B-Video-Size calculated to %ux%u',
  199. bsize[0], bsize[1])
  200. try:
  201. bpos = [int(i) for i in Config.get('side-by-side-preview',
  202. 'bpos').split('/', 1)]
  203. self.log.debug('B-Video-Position configured to %u/%u',
  204. bpos[0], bpos[1])
  205. except:
  206. bpos = [
  207. width - int(width / 100) - bsize[0],
  208. height - int(width / 100) - bsize[1] # 1%
  209. ]
  210. self.log.debug('B-Video-Position calculated to %u/%u',
  211. bpos[0], bpos[1])
  212. for idx, name in enumerate(self.names):
  213. pad = self.padState[idx]
  214. pad.reset()
  215. if idx == self.sourceA:
  216. pad.xpos, pad.ypos = apos
  217. pad.width, pad.height = asize
  218. pad.zorder = 1
  219. elif idx == self.sourceB:
  220. pad.xpos, pad.ypos = bpos
  221. pad.width, pad.height = bsize
  222. pad.zorder = 2
  223. else:
  224. pad.alpha = 0
  225. def recalculateMixerStatePictureInPicture(self):
  226. self.log.info('Updating Mixer-State for '
  227. 'Picture-in-Picture-Composition')
  228. width, height = self.getInputVideoSize()
  229. self.log.debug('Video-Size parsed as %ux%u', width, height)
  230. try:
  231. pipsize = [int(i) for i in Config.get('picture-in-picture',
  232. 'pipsize').split('x', 1)]
  233. self.log.debug('PIP-Size configured to %ux%u',
  234. pipsize[0], pipsize[1])
  235. except:
  236. pipsize = [
  237. int(width / 4), # 25%
  238. int(height / 4) # 25%
  239. ]
  240. self.log.debug('PIP-Size calculated to %ux%u',
  241. pipsize[0], pipsize[1])
  242. try:
  243. pippos = [int(i) for i in Config.get('picture-in-picture',
  244. 'pippos').split('/', 1)]
  245. self.log.debug('PIP-Position configured to %u/%u',
  246. pippos[0], pippos[1])
  247. except:
  248. pippos = [
  249. width - pipsize[0] - int(width / 100), # 1%
  250. height - pipsize[1] - int(width / 100) # 1%
  251. ]
  252. self.log.debug('PIP-Position calculated to %u/%u',
  253. pippos[0], pippos[1])
  254. for idx, name in enumerate(self.names):
  255. pad = self.padState[idx]
  256. pad.reset()
  257. if idx == self.sourceA:
  258. pass
  259. elif idx == self.sourceB:
  260. pad.xpos, pad.ypos = pippos
  261. pad.width, pad.height = pipsize
  262. pad.zorder = 2
  263. else:
  264. pad.alpha = 0
  265. def recalculateMixerStateMatrixTwoByTwo(self):
  266. self.log.info('Updating Mixer-State for '
  267. 'Matrix-Two-by-two-Composition')
  268. width, height = self.getInputVideoSize()
  269. self.log.debug('Video-Size parsed as %ux%u', width, height)
  270. try:
  271. gutter = Config.getint('matrix-two-by-two', 'gutter')
  272. self.log.debug('Gutter configured to %u', gutter)
  273. except:
  274. gutter = int(width / 100)
  275. self.log.debug('Gutter calculated to %u', gutter)
  276. targetWidth = int((width - gutter) / 2)
  277. #targetHeight = int(targetWidth / width * height)
  278. targetHeight = int((height - gutter) / 2)
  279. self.log.debug('Video-Size calculated to %ux%u',
  280. targetWidth, targetHeight)
  281. xa = 0
  282. xb = width - targetWidth
  283. y1 = 0
  284. y2 = height - targetHeight
  285. for idx, name in enumerate(self.names):
  286. pad = self.padState[idx]
  287. pad.reset()
  288. pad.width = targetWidth
  289. pad.height = targetHeight
  290. if idx == self.sourceA:
  291. pad.xpos = xa
  292. pad.ypos = y1
  293. pad.zorder = 1
  294. elif idx == self.sourceB:
  295. pad.xpos = xb
  296. pad.ypos = y1
  297. pad.zorder = 2
  298. elif idx == self.sourceC:
  299. pad.xpos = xa
  300. pad.ypos = y2
  301. pad.zorder = 3
  302. elif idx == self.sourceD:
  303. pad.xpos = xb
  304. pad.ypos = y2
  305. pad.zorder = 4
  306. else:
  307. pad.alpha = 0
  308. def applyMixerState(self):
  309. for idx, state in enumerate(self.padState):
  310. # mixerpad 0 = background
  311. mixerpad = (self.mixingPipeline
  312. .get_by_name('mix')
  313. .get_static_pad('sink_%u' % (idx + 1)))
  314. self.log.debug('Reconfiguring Mixerpad %u to '
  315. 'x/y=%u/%u, w/h=%u/%u alpha=%0.2f, zorder=%u',
  316. idx, state.xpos, state.ypos,
  317. state.width, state.height,
  318. state.alpha, state.zorder)
  319. mixerpad.set_property('xpos', state.xpos)
  320. mixerpad.set_property('ypos', state.ypos)
  321. mixerpad.set_property('width', state.width)
  322. mixerpad.set_property('height', state.height)
  323. mixerpad.set_property('alpha', state.alpha)
  324. mixerpad.set_property('zorder', state.zorder)
  325. def selectCompositeModeDefaultSources(self):
  326. sectionNames = {
  327. CompositeModes.fullscreen: 'fullscreen',
  328. CompositeModes.side_by_side_equal: 'side-by-side-equal',
  329. CompositeModes.side_by_side_preview: 'side-by-side-preview',
  330. CompositeModes.picture_in_picture: 'picture-in-picture',
  331. CompositeModes.matrix_two_by_two: 'matrix-two-by-two'
  332. }
  333. compositeModeName = self.compositeMode.name
  334. sectionName = sectionNames[self.compositeMode]
  335. try:
  336. defSource = Config.get(sectionName, 'default-a')
  337. self.setVideoSourceA(self.names.index(defSource))
  338. self.log.info('Changing sourceA to default of Mode %s: %s',
  339. compositeModeName, defSource)
  340. except Exception as e:
  341. pass
  342. try:
  343. defSource = Config.get(sectionName, 'default-b')
  344. self.setVideoSourceB(self.names.index(defSource))
  345. self.log.info('Changing sourceB to default of Mode %s: %s',
  346. compositeModeName, defSource)
  347. except Exception as e:
  348. pass
  349. try:
  350. defSource = Config.get(sectionName, 'default-c')
  351. self.setVideoSourceC(self.names.index(defSource))
  352. self.log.info('Changing sourceC to default of Mode %s: %s',
  353. compositeModeName, defSource)
  354. except Exception as e:
  355. pass
  356. try:
  357. defSource = Config.get(sectionName, 'default-d')
  358. self.setVideoSourceD(self.names.index(defSource))
  359. self.log.info('Changing sourceD to default of Mode %s: %s',
  360. compositeModeName, defSource)
  361. except Exception as e:
  362. pass
  363. def on_handoff(self, object, buffer):
  364. if self.padStateDirty:
  365. self.padStateDirty = False
  366. self.log.debug('[Streaming-Thread]: Pad-State is Dirty, '
  367. 'applying new Mixer-State')
  368. self.applyMixerState()
  369. def on_eos(self, bus, message):
  370. self.log.debug('Received End-of-Stream-Signal on Mixing-Pipeline')
  371. def on_error(self, bus, message):
  372. self.log.debug('Received Error-Signal on Mixing-Pipeline')
  373. (error, debug) = message.parse_error()
  374. self.log.debug('Error-Details: #%u: %s', error.code, debug)
  375. def setVideoSourceA(self, source):
  376. # swap if required
  377. if self.sourceB == source:
  378. self.sourceB = self.sourceA
  379. self.sourceA = source
  380. self.recalculateMixerState()
  381. def getVideoSourceA(self):
  382. return self.sourceA
  383. def setVideoSourceB(self, source):
  384. # swap if required
  385. if self.sourceA == source:
  386. self.sourceA = self.sourceB
  387. self.sourceB = source
  388. self.recalculateMixerState()
  389. def getVideoSourceB(self):
  390. return self.sourceB
  391. def setCompositeMode(self, mode):
  392. self.compositeMode = mode
  393. self.selectCompositeModeDefaultSources()
  394. self.recalculateMixerState()
  395. def getCompositeMode(self):
  396. return self.compositeMode