diff --git a/voctocore/default-config.ini b/voctocore/default-config.ini index 3db46b3b..75723dab 100644 --- a/voctocore/default-config.ini +++ b/voctocore/default-config.ini @@ -1,27 +1,34 @@ [mix] -videocaps=video/x-raw,format=I420,width=1920,height=1080,framerate=25/1,pixel-aspect-ratio=1/1 -audiocaps=audio/x-raw,format=S16LE,channels=2,layout=interleaved,rate=48000 +videocaps = video/x-raw,format=I420,width=1920,height=1080,framerate=25/1,pixel-aspect-ratio=1/1,interlace-mode=progressive +audiocaps = audio/x-raw,format=S16LE,channels=2,layout=interleaved,rate=48000 ; tcp-ports will be 10000,10001,10002 -sources=cam1,cam2,grabber +sources = cam1,cam2,grabber ; set the initial audio source (shortcut for setting the volume of the ; audio-sources to 1.0), defaults to the first source -;audiosource=cam1 +;audiosource = cam1 ;[source.cam1] -;kind=decklink -;devicenumber=0 -;video_connection=SDI -;video_mode=1080i50 -;audio_connection=embedded +;kind = decklink +;devicenumber = 0 +;video_connection = SDI +;video_mode = 1080i50 +;audio_connection = embedded +;deinterlace = yes +;deinterlace = no +;volume=0.5 ;[source.cam2] -;volume=0.5 +;kind = tcp +;deinterlace = yes +;deinterlace = no +;deinterlace = assume-progressive +;volume = 0.5 ;[source.background] -;kind=img -;imguri=file:///opt/voc/share/background.png +;kind = img +;imguri = file:///opt/voc/share/background.png [output-buffers] @@ -35,63 +42,63 @@ sources=cam1,cam2,grabber ; you might want to up that even more for your recording-sink, so that it never ; gets disconnected. for this reason, the following configuration raises the ; default limit for the mix_out sink to a whopping 10'000 frames (400 seconds) -;cam1_mirror=500 -;cam2_mirror=500 -;grabber_mirror=500 -mix_out=10000 -;streamblanker_out=500 +;cam1_mirror = 500 +;cam2_mirror = 500 +;grabber_mirror = 500 +mix_out = 10000 +;streamblanker_out = 500 [fullscreen] ; if configured, switching to fullscreen will automatically select this ; source. if not configured, it will not change the last set source -;default-a=cam1 +;default-a = cam1 [side-by-side-equal] ; defaults to 1% of the video width -;gutter=12 -;atop=50 -;btop=200 +;gutter = 12 +;atop = 50 +;btop = 200 ; if configured, switching to the sbs-equal mode will automatically select these ; sources. if not configured, it will not change the last set sources -;default-a=cam1 -;default-b=cam2 +;default-a = cam1 +;default-b = cam2 [side-by-side-preview] -;asize=1024x576 -;apos=12/12 -;bsize=320x180 -;bpos=948/528 +;asize = 1024x576 +;apos = 12/12 +;bsize = 320x180 +;bpos = 948/528 ; automatically select these sources when switching to sbs-preview -;default-a=grabber -;default-b=cam1 +;default-a = grabber +;default-b = cam1 [picture-in-picture] -;pipsize=320x180 -;pippos=948/528 +;pipsize = 320x180 +;pippos = 948/528 ; automatically select these sources when switching to pip -;default-a=grabber -;default-b=cam1 +;default-a = grabber +;default-b = cam1 [previews] ; disable if ui & server run on the same computer and can exchange uncompressed video frames -enabled=false -deinterlace=false +enabled = false +deinterlace = false ; use vaapi to encode the previews, can be h264, mpeg2 or jpeg (BUT ONLY h264 IS TESTED) ; not all encoders are available on all CPUs -;vaapi=h264 +;vaapi = h264 ; default to mix-videocaps, only applicable if enabled=true ; you can change the framerate and the width/height, but nothing else -;videocaps=video/x-raw,width=1024,height=576,framerate=25/1 +;videocaps = video/x-raw,width=1024,height=576,framerate=25/1 [stream-blanker] -enabled=true -sources=pause,nostream -volume=1.0 +enabled = true +sources = pause,nostream +volume = 1.0 ;[source.stream-blanker-pause] ;kind=img @@ -99,4 +106,4 @@ volume=1.0 [mirrors] ; disable if not needed -enabled=true +enabled = true diff --git a/voctocore/lib/sources/avsource.py b/voctocore/lib/sources/avsource.py index 3c03354f..7e6e0440 100644 --- a/voctocore/lib/sources/avsource.py +++ b/voctocore/lib/sources/avsource.py @@ -7,7 +7,6 @@ class AVSource(object, metaclass=ABCMeta): - def __init__(self, name, outputs=None, has_audio=True, has_video=True): if not self.log: self.log = logging.getLogger('AVSource[{}]'.format(name)) @@ -55,6 +54,7 @@ def build_pipeline(self, pipeline, aelem=None, velem=None): tee name=vtee """.format( velem=velem, + deinterlacer=self.build_deinterlacer(), vcaps=Config.get('mix', 'videocaps') ) @@ -74,6 +74,25 @@ def build_pipeline(self, pipeline, aelem=None, velem=None): self.pipeline.bus.connect("message::eos", self.on_eos) self.pipeline.bus.connect("message::error", self.on_error) + def build_deinterlacer(self): + deinterlace_config = self.get_deinterlace_config() + + if deinterlace_config == "yes": + return "yadif mode=interlaced" + + elif deinterlace_config == "no": + return "" + + else: + raise RuntimeError( + "Unknown Deinterlace-Mode on source {} configured: {}" + .format(self.name, deinterlace_config)) + + def get_deinterlace_config(self): + section = 'source.{}'.format(self.name) + deinterlace_config = Config.get(section, 'deinterlace', fallback="no") + return deinterlace_config + def on_eos(self, bus, message): self.log.debug('Received End-of-Stream-Signal on Source-Pipeline') diff --git a/voctocore/lib/sources/decklinkavsource.py b/voctocore/lib/sources/decklinkavsource.py index a1219892..f12d262b 100644 --- a/voctocore/lib/sources/decklinkavsource.py +++ b/voctocore/lib/sources/decklinkavsource.py @@ -6,18 +6,21 @@ class DeckLinkAVSource(AVSource): - def __init__(self, name, outputs=None, has_audio=True, has_video=True): self.log = logging.getLogger('DecklinkAVSource[{}]'.format(name)) super().__init__(name, outputs, has_audio, has_video) section = 'source.{}'.format(name) + # Device number, default: 0 self.device = Config.get(section, 'devicenumber', fallback=0) + # Audio connection, default: Automatic self.aconn = Config.get(section, 'audio_connection', fallback='auto') + # Video connection, default: Automatic self.vconn = Config.get(section, 'video_connection', fallback='auto') + # Video mode, default: 1080i50 self.vmode = Config.get(section, 'video_mode', fallback='1080i50') @@ -45,10 +48,12 @@ def launch_pipeline(self): if self.has_video: pipeline += """ videoconvert ! - yadif ! + {deinterlacer} videoscale ! videorate name=deckvideo - """ + """.format( + deinterlacer=self.build_deinterlacer() + ) else: pipeline += """ fakesink @@ -67,6 +72,13 @@ def launch_pipeline(self): self.build_pipeline(pipeline, aelem='deckaudio', velem='deckvideo') self.pipeline.set_state(Gst.State.PLAYING) + def build_deinterlacer(self): + deinterlacer = super().build_deinterlacer() + if deinterlacer != '': + deinterlacer += ' !' + + return deinterlacer + def restart(self): self.pipeline.set_state(Gst.State.NULL) self.launch_pipeline() diff --git a/voctocore/lib/sources/tcpavsource.py b/voctocore/lib/sources/tcpavsource.py index 1d81e29e..74ab46e2 100644 --- a/voctocore/lib/sources/tcpavsource.py +++ b/voctocore/lib/sources/tcpavsource.py @@ -10,7 +10,6 @@ class TCPAVSource(AVSource, TCPSingleConnection): - def __init__(self, name, port, outputs=None, has_audio=True, has_video=True): self.log = logging.getLogger('TCPAVSource[{}]'.format(name)) @@ -24,6 +23,7 @@ def __str__(self): ) def on_accepted(self, conn, addr): + deinterlacer = self.build_deinterlacer() pipeline = """ fdsrc fd={fd} blocksize=1048576 ! queue ! @@ -31,7 +31,19 @@ def on_accepted(self, conn, addr): """.format( fd=conn.fileno() ) - self.build_pipeline(pipeline, aelem='demux', velem='demux') + + if deinterlacer: + pipeline += """ + demux. ! + video/x-raw ! + {deinterlacer} + """.format( + deinterlacer=self.build_deinterlacer() + ) + self.build_pipeline(pipeline, aelem='demux', velem='deinter') + + else: + self.build_pipeline(pipeline, aelem='demux', velem='demux') self.audio_caps = Gst.Caps.from_string(Config.get('mix', 'audiocaps')) self.video_caps = Gst.Caps.from_string(Config.get('mix', 'videocaps')) @@ -41,6 +53,20 @@ def on_accepted(self, conn, addr): self.pipeline.set_state(Gst.State.PLAYING) + def build_deinterlacer(self): + deinterlace_config = self.get_deinterlace_config() + + if deinterlace_config == "assume-progressive": + deinterlacer = "capssetter " \ + "caps=video/x-raw,interlace-mode=progressive" + else: + deinterlacer = super().build_deinterlacer() + + if deinterlacer != '': + deinterlacer += ' name=deinter' + + return deinterlacer + def on_pad_added(self, demux, src_pad): caps = src_pad.query_caps(None) self.log.debug('demuxer added pad w/ caps: %s', caps.to_string()) @@ -66,6 +92,8 @@ def on_pad_added(self, demux, src_pad): self.log.warning(' configured caps: %s', self.video_caps.to_string()) + self.test_and_warn_interlace_mode(caps) + def on_eos(self, bus, message): super().on_eos(bus, message) if self.currentConnection is not None: @@ -84,3 +112,19 @@ def disconnect(self): def restart(self): if self.currentConnection is not None: self.disconnect() + + def test_and_warn_interlace_mode(self, caps): + interlace_mode = caps.get_structure(0).get_string('interlace-mode') + deinterlace_config = self.get_deinterlace_config() + + if interlace_mode == 'mixed' and deinterlace_config == 'no': + self.log.warning( + 'your source sent an interlace_mode-flag in the matroska-' + 'container, specifying the source-video-stream is of ' + 'mixed-mode.\n' + 'this is probably a gstreamer-bug which is triggered with ' + 'recent ffmpeg-versions\n' + 'setting [source.{name}] deinterlace=assume-progressive ' + 'might help see https://github.com/voc/voctomix/issues/137 ' + 'for more information'.format(name=self.name) + ) diff --git a/voctocore/tests/helper/voctomix_test.py b/voctocore/tests/helper/voctomix_test.py index 4cbb4253..59da9af6 100644 --- a/voctocore/tests/helper/voctomix_test.py +++ b/voctocore/tests/helper/voctomix_test.py @@ -1,3 +1,4 @@ +import socket import unittest import gi.repository @@ -11,6 +12,8 @@ gi.repository.GObject = MagicMock() lib.config.Config = ConfigMock.WithBasicConfig() +socket.socket = MagicMock() + class VoctomixTest(unittest.TestCase): """Base-Class for all Voctomix-Tests""" diff --git a/voctocore/tests/sources/__init__.py b/voctocore/tests/sources/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/voctocore/tests/sources/avsource/__init__.py b/voctocore/tests/sources/avsource/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/voctocore/tests/sources/avsource/decklinkavsource/__init__.py b/voctocore/tests/sources/avsource/decklinkavsource/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/voctocore/tests/sources/avsource/decklinkavsource/test_deinterlacer_setting.py b/voctocore/tests/sources/avsource/decklinkavsource/test_deinterlacer_setting.py new file mode 100644 index 00000000..e9a63251 --- /dev/null +++ b/voctocore/tests/sources/avsource/decklinkavsource/test_deinterlacer_setting.py @@ -0,0 +1,46 @@ +from tests.helper.voctomix_test import VoctomixTest +from gi.repository import Gst +from lib.sources import DeckLinkAVSource +from lib.config import Config + + +# noinspection PyUnusedLocal +class AudiomixMultipleSources(VoctomixTest): + def setUp(self): + super().setUp() + + Config.given("mix", "videocaps", "video/x-raw") + Config.given("source.cam1", "kind", "decklink") + Config.given("source.cam1", "devicenumber", "23") + + self.source = DeckLinkAVSource('cam1', ['test_mixer', 'test_preview'], has_audio=True, has_video=True) + + def test_unconfigured_does_not_add_a_deinterlacer(self): + pipeline = self.simulate_connection_and_aquire_pipeline_description() + self.assertRegexAnyWhitespace(pipeline, "videoconvert ! videoscale") + + def test_no_does_not_add_a_deinterlacer(self): + Config.given("source.cam1", "deinterlace", "no") + + pipeline = self.simulate_connection_and_aquire_pipeline_description() + self.assertRegexAnyWhitespace(pipeline, "videoconvert ! videoscale") + + def test_yes_does_add_yadif(self): + Config.given("source.cam1", "deinterlace", "yes") + + pipeline = self.simulate_connection_and_aquire_pipeline_description() + self.assertRegexAnyWhitespace(pipeline, "videoconvert ! yadif mode=interlaced ! videoscale") + + def simulate_connection_and_aquire_pipeline_description(self): + Gst.parse_launch.reset_mock() + self.source.launch_pipeline() + + Gst.parse_launch.assert_called() + args, kwargs = Gst.parse_launch.call_args_list[0] + pipeline = args[0] + + return pipeline + + def assertRegexAnyWhitespace(self, text, regex): + regex = regex.replace(" ", "\s*") + self.assertRegex(text, regex) diff --git a/voctocore/tests/sources/avsource/tcpavsource/__init__.py b/voctocore/tests/sources/avsource/tcpavsource/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/voctocore/tests/sources/avsource/tcpavsource/test_deinterlacer_setting.py b/voctocore/tests/sources/avsource/tcpavsource/test_deinterlacer_setting.py new file mode 100644 index 00000000..dcbedfd3 --- /dev/null +++ b/voctocore/tests/sources/avsource/tcpavsource/test_deinterlacer_setting.py @@ -0,0 +1,58 @@ +import io + +from mock import MagicMock + +from tests.helper.voctomix_test import VoctomixTest +from gi.repository import Gst +from lib.sources import TCPAVSource +from lib.config import Config + + +# noinspection PyUnusedLocal +class AudiomixMultipleSources(VoctomixTest): + def setUp(self): + super().setUp() + + Config.given("mix", "videocaps", "video/x-raw") + + self.source = TCPAVSource('cam1', 42, ['test_mixer', 'test_preview'], has_audio=True, has_video=True) + self.mock_fp = MagicMock(spec=io.IOBase) + + def test_unconfigured_does_not_add_a_deinterlacer(self): + pipeline = self.simulate_connection_and_aquire_pipeline_description() + self.assertRegexAnyWhitespace(pipeline, "demux. ! video/x-raw ! queue ! tee name=vtee") + + def test_no_does_not_add_a_deinterlacer(self): + Config.given("source.cam1", "deinterlace", "no") + + pipeline = self.simulate_connection_and_aquire_pipeline_description() + self.assertRegexAnyWhitespace(pipeline, "demux. ! video/x-raw ! queue ! tee name=vtee") + + def test_yes_does_add_yadif(self): + Config.given("source.cam1", "deinterlace", "yes") + + pipeline = self.simulate_connection_and_aquire_pipeline_description() + self.assertRegexAnyWhitespace(pipeline, "demux. ! video/x-raw ! yadif mode=interlaced name=deinter") + + def test_assume_progressive_does_add_capssetter(self): + Config.given("source.cam1", "deinterlace", "assume-progressive") + + pipeline = self.simulate_connection_and_aquire_pipeline_description() + self.assertRegexAnyWhitespace( + pipeline, + "demux. ! video/x-raw ! capssetter caps=video/x-raw,interlace-mode=progressive name=deinter" + ) + + def simulate_connection_and_aquire_pipeline_description(self): + Gst.parse_launch.reset_mock() + self.source.on_accepted(self.mock_fp, '127.0.0.42') + + Gst.parse_launch.assert_called() + args, kwargs = Gst.parse_launch.call_args_list[0] + pipeline = args[0] + + return pipeline + + def assertRegexAnyWhitespace(self, text, regex): + regex = regex.replace(" ", "\s*") + self.assertRegex(text, regex)