aboutsummaryrefslogtreecommitdiff
path: root/voctocore/lib/videomix.py
blob: f98582ae17b8bcb01b6064ed946ea615d7dd2091 (plain)
  1. import sys, inspect, math
  2. from pprint import pprint
  3. from gi.repository import GLib, Gst
  4. from lib.controlserver import controlServerEntrypoint
  5. class Videomix:
  6. """mixing, streaming and encoding pipeline constuction and control"""
  7. # size of the monitor-streams
  8. # should be anamorphic PAL, beacuse we encode it to dv and send it to the mixer-gui
  9. monitorSize = (1024, 576)
  10. previewbins = []
  11. def __init__(self):
  12. """initialize video mixing, streaming and encoding pipeline"""
  13. # initialize an empty pipeline
  14. self.pipeline = Gst.Pipeline()
  15. # create audio and video mixer
  16. mixerbin = self.createMixer()
  17. # collection of video-sources to connect to the quadmix
  18. quadmixSources = []
  19. # create camera sources
  20. for camberabin in self.createDummyCamSources():
  21. # link camerasource to audiomixer
  22. camberabin.get_by_name('audio_src').link(self.pipeline.get_by_name('liveaudio'))
  23. # inject a ×2 distributor and link one end to the live-mixer
  24. distributor = self.createDistributor(camberabin.get_by_name('video_src'), camberabin.get_name())
  25. distributor.get_by_name('a').link(self.pipeline.get_by_name('livevideo'))
  26. # collect the other end to add it later to the quadmix
  27. quadmixSources.append(distributor.get_by_name('b'))
  28. # TODO: generate pause & slides with another generator here which only
  29. # yields if the respective files are present and which only have a video-pad
  30. # add all video-sources to the quadmix-monitor-screen
  31. self.addVideosToQuadmix(quadmixSources, self.pipeline.get_by_name('quadmix'))
  32. # initialize to known defaults
  33. # TODO: make configurable
  34. self.switchVideo(0)
  35. self.switchAudio(0)
  36. Gst.debug_bin_to_dot_file(self.pipeline, Gst.DebugGraphDetails.ALL, 'test')
  37. self.pipeline.set_state(Gst.State.PLAYING)
  38. def createMixer(self):
  39. """create audio and video mixer"""
  40. # create mixer-pipeline from string
  41. mixerbin = Gst.parse_bin_from_description("""
  42. videomixer name=livevideo ! autovideosink
  43. input-selector name=liveaudio ! autoaudiosink
  44. videotestsrc pattern="solid-color" foreground-color=0x808080 ! capsfilter name=filter ! videomixer name=quadmix ! autovideosink
  45. """, False)
  46. # define caps for the videotestsrc which generates the background-color for the quadmix
  47. bgcaps = Gst.Caps.new_empty_simple('video/x-raw')
  48. bgcaps.set_value('width', round(self.monitorSize[0]))
  49. bgcaps.set_value('height', round(self.monitorSize[1]))
  50. mixerbin.get_by_name('filter').set_property('caps', bgcaps)
  51. # name the bin, add and return it
  52. mixerbin.set_name('mixerbin')
  53. self.pipeline.add(mixerbin)
  54. return mixerbin
  55. def addVideosToQuadmix(self, videosources, quadmix):
  56. """add all avaiable videosources to the quadmix"""
  57. count = len(videosources)
  58. # coordinate of the cell where we place the next video
  59. place = [0, 0]
  60. # number of cells in the quadmix-monitor
  61. grid = [0, 0]
  62. grid[0] = math.ceil(math.sqrt(count))
  63. grid[1] = math.ceil(count / grid[0])
  64. # size of each cell in the quadmix-monitor
  65. cellSize = (
  66. self.monitorSize[0] / grid[0],
  67. self.monitorSize[1] / grid[1]
  68. )
  69. print("showing {} videosources in a {}×{} grid in a {}×{} px window, which gives cells of {}×{} px per videosource".format(
  70. count, grid[0], grid[1], self.monitorSize[0], self.monitorSize[1], cellSize[0], cellSize[1]))
  71. # iterate over all video-sources
  72. for idx, videosource in enumerate(videosources):
  73. # generate a pipeline for this videosource which
  74. # - scales the video to the request
  75. # - remove n px of the video (n = 5 if the video is highlighted else 0)
  76. # - add a colored border of n px of the video (n = 5 if the video is highlighted else 0)
  77. # - overlay the index of the video as text in the top left corner
  78. # - known & named output
  79. previewbin = Gst.parse_bin_from_description("""
  80. videoscale name=in !
  81. capsfilter name=caps !
  82. videobox name=crop top=0 left=0 bottom=0 right=0 !
  83. videobox fill=red top=-0 left=-0 bottom=-0 right=-0 name=add !
  84. textoverlay color=0xFFFFFFFF halignment=left valignment=top xpad=10 ypad=5 font-desc="sans 35" name=text !
  85. identity name=out
  86. """, False)
  87. # name the bin and add it
  88. previewbin.set_name('previewbin-{}'.format(idx))
  89. self.pipeline.add(previewbin)
  90. self.previewbins.append(previewbin)
  91. # set the overlay-text
  92. previewbin.get_by_name('text').set_property('text', str(idx))
  93. # query the video-source caps and extract its size
  94. caps = videosource.get_static_pad('src').query_caps(None)
  95. capsstruct = caps.get_structure(0)
  96. srcSize = (
  97. capsstruct.get_int('width')[1],
  98. capsstruct.get_int('height')[1],
  99. )
  100. # calculate the ideal scale factor and scale the sizes
  101. f = max(srcSize[0] / cellSize[0], srcSize[1] / cellSize[1])
  102. scaleSize = (
  103. srcSize[0] / f,
  104. srcSize[1] / f,
  105. )
  106. # calculate the top/left coordinate
  107. coord = (
  108. place[0] * cellSize[0] + (cellSize[0] - scaleSize[0]) / 2,
  109. place[1] * cellSize[1] + (cellSize[1] - scaleSize[1]) / 2,
  110. )
  111. print("placing videosource {} of size {}×{} scaled by {} to {}×{} in a cell {}×{} px cell ({}/{}) at position ({}/{})".format(
  112. idx, srcSize[0], srcSize[1], f, scaleSize[0], scaleSize[1], cellSize[0], cellSize[1], place[0], place[1], coord[0], coord[1]))
  113. # link the videosource to the input of the preview-bin
  114. videosource.link(previewbin.get_by_name('in'))
  115. # create and set the caps for the preview-scaler
  116. scalecaps = Gst.Caps.new_empty_simple('video/x-raw')
  117. scalecaps.set_value('width', round(scaleSize[0]))
  118. scalecaps.set_value('height', round(scaleSize[1]))
  119. previewbin.get_by_name('caps').set_property('caps', scalecaps)
  120. # request a pad from the quadmixer and configure x/y position
  121. sinkpad = quadmix.get_request_pad('sink_%u')
  122. sinkpad.set_property('xpos', round(coord[0]))
  123. sinkpad.set_property('ypos', round(coord[1]))
  124. # link the output of the preview-bin to the mixer
  125. previewbin.get_by_name('out').link(quadmix)
  126. # increment grid position
  127. place[0] += 1
  128. if place[0] >= grid[0]:
  129. place[1] += 1
  130. place[0] = 0
  131. def createDistributor(self, videosource, name):
  132. """create a simple ×2 distributor"""
  133. distributor = Gst.parse_bin_from_description("""
  134. tee name=t
  135. t. ! queue name=a
  136. t. ! queue name=b
  137. """, False)
  138. # set a name and add to pipeline
  139. distributor.set_name('distributor({0})'.format(name))
  140. self.pipeline.add(distributor)
  141. # link input to the tee
  142. videosource.link(distributor.get_by_name('t'))
  143. return distributor
  144. def createDummyCamSources(self):
  145. """create test-video-sources from files or urls"""
  146. # TODO make configurable
  147. uris = ('file:///home/peter/122.mp4', 'file:///home/peter/10025.mp4',)
  148. for idx, uri in enumerate(uris):
  149. # create a bin for a simulated camera input
  150. # force the input resolution to 1024x576 because that way the following elements
  151. # in the pipeline cam know the size even if the file is not yet loaded. the quadmixer
  152. # is not resize-capable
  153. camberabin = Gst.parse_bin_from_description("""
  154. uridecodebin name=input
  155. input. ! videoconvert ! videoscale ! videorate ! video/x-raw,width=1024,height=576,framerate=25/1 ! identity name=video_src
  156. input. ! audioconvert name=audio_src
  157. """, False)
  158. # set name and uri
  159. camberabin.set_name('dummy-camberabin({0})'.format(uri))
  160. camberabin.get_by_name('input').set_property('uri', uri)
  161. # add to pipeline and pass the bin upstream
  162. self.pipeline.add(camberabin)
  163. yield camberabin
  164. def createCamSources(self):
  165. """create real-video-sources from the bmd-drivers"""
  166. # TODO make number of installed cams configurable
  167. for cam in range(2):
  168. # create a bin for camera input
  169. camberabin = Gst.parse_bin_from_description("""
  170. decklinksrc name=input input=sdi input-mode=1080p25
  171. input. ! videoconvert ! videoscale ! videorate ! video/x-raw,width=1920,height=1080,framerate=25/1 ! identity name=video_src
  172. input. ! audioconvert name=audio_src
  173. """, False)
  174. # set name and subdevice
  175. camberabin.set_name('camberabin({0})'.format(cam))
  176. camberabin.get_by_name('input').set_property('subdevice', cam)
  177. # add to pipeline and pass the bin upstream
  178. self.pipeline.add(camberabin)
  179. yield camberabin
  180. def iteratorHelper(self, it):
  181. while True:
  182. result, value = it.next()
  183. if result == Gst.IteratorResult.DONE:
  184. break
  185. if result != Gst.IteratorResult.OK:
  186. raise IteratorError(result)
  187. yield value
  188. def previewBorderHelper(self, previewbin, enabled, color = 'red'):
  189. crop = previewbin.get_by_name('crop')
  190. add = previewbin.get_by_name('add')
  191. add.set_property('fill', color)
  192. for side in ('top', 'left', 'right', 'bottom'):
  193. crop.set_property(side, 5 if enabled else 0)
  194. add.set_property(side, -5 if enabled else 0)
  195. ### below are access-methods for the ControlServer
  196. @controlServerEntrypoint
  197. def numAudioSources(self):
  198. """return number of available audio sources"""
  199. liveaudio = self.pipeline.get_by_name('liveaudio')
  200. return str(len(list(self.iteratorHelper(liveaudio.iterate_sink_pads()))))
  201. @controlServerEntrypoint
  202. def switchAudio(self, audiosource):
  203. """switch audio to the selected audio"""
  204. liveaudio = self.pipeline.get_by_name('liveaudio')
  205. pad = liveaudio.get_static_pad('sink_{}'.format(audiosource))
  206. if pad is None:
  207. return 'unknown audio-source: {}'.format(audiosource)
  208. liveaudio.set_property('active-pad', pad)
  209. return True
  210. @controlServerEntrypoint
  211. def numVideoSources(self):
  212. """return number of available video sources"""
  213. livevideo = self.pipeline.get_by_name('livevideo')
  214. return str(len(list(self.iteratorHelper(livevideo.iterate_sink_pads()))))
  215. @controlServerEntrypoint
  216. def switchVideo(self, videosource):
  217. """switch audio to the selected video"""
  218. livevideo = self.pipeline.get_by_name('livevideo')
  219. pad = livevideo.get_static_pad('sink_{}'.format(videosource))
  220. previewbin = self.pipeline.get_by_name('previewbin-{}'.format(videosource))
  221. if pad is None or previewbin is None:
  222. return 'unknown video-source: {}'.format(videosource)
  223. self.previewBorderHelper(previewbin, True, 'green')
  224. for iterbin in self.previewbins:
  225. if previewbin != iterbin:
  226. self.previewBorderHelper(iterbin, False)
  227. pad.set_property('alpha', 1)
  228. for iterpad in self.iteratorHelper(livevideo.iterate_sink_pads()):
  229. if pad != iterpad:
  230. #self.previewBorderHelper(iterpad, 0)
  231. iterpad.set_property('alpha', 0)
  232. @controlServerEntrypoint
  233. def fadeVideo(self, videosource):
  234. """fade video to the selected video"""
  235. raise NotImplementedError("fade command is not implemented yet")
  236. @controlServerEntrypoint
  237. def setPipVideo(self, videosource):
  238. """switch video-source in the PIP to the selected video"""
  239. raise NotImplementedError("pip commands are not implemented yet")
  240. @controlServerEntrypoint
  241. def fadePipVideo(self, videosource):
  242. """fade video-source in the PIP to the selected video"""
  243. raise NotImplementedError("pip commands are not implemented yet")
  244. class PipPlacements:
  245. """enumeration of possible PIP-Placements"""
  246. TopLeft, TopRight, BottomLeft, BottomRight = range(4)
  247. @controlServerEntrypoint
  248. def setPipPlacement(self, placement):
  249. """place PIP in the selected position"""
  250. assert(isinstance(placement, PipPlacements))
  251. raise NotImplementedError("pip commands are not implemented yet")
  252. @controlServerEntrypoint
  253. def setPipStatus(self, enabled):
  254. """show or hide PIP"""
  255. raise NotImplementedError("pip commands are not implemented yet")
  256. @controlServerEntrypoint
  257. def fadePipStatus(self, enabled):
  258. """fade PIP in our out"""
  259. raise NotImplementedError("pip commands are not implemented yet")
  260. class StreamContents:
  261. """enumeration of possible PIP-Placements"""
  262. Live, Pause, NoStream = range(3)
  263. @controlServerEntrypoint
  264. def selectStreamContent(self, content):
  265. """switch the livestream-content between selected mixer output, pause-image or nostream-imag"""
  266. assert(isinstance(content, StreamContents))
  267. raise NotImplementedError("pause/nostream switching is not implemented yet")