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