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