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