From 8a6830a0400b18cc81c8bea9e117c4d8c07fd060 Mon Sep 17 00:00:00 2001 From: Luke Moscrop Date: Fri, 14 Dec 2018 10:10:40 +0000 Subject: [PATCH 1/3] Added Decklink Input --- brave/inputs/__init__.py | 3 ++ brave/inputs/decklink.py | 108 +++++++++++++++++++++++++++++++++++++++ public/js/inputs.js | 85 +++++++++++++++++++++++++++++- 3 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 brave/inputs/decklink.py diff --git a/brave/inputs/__init__.py b/brave/inputs/__init__.py index 5e5cf4e..b56c276 100644 --- a/brave/inputs/__init__.py +++ b/brave/inputs/__init__.py @@ -3,6 +3,7 @@ from brave.inputs.test_audio import TestAudioInput from brave.inputs.image import ImageInput from brave.inputs.html import HTMLInput +from brave.inputs.decklink import DecklinkInput from brave.abstract_collection import AbstractCollection import brave.exceptions @@ -23,6 +24,8 @@ def add(self, **args): input = ImageInput(**args, collection=self) elif args['type'] == 'html': input = HTMLInput(**args, collection=self) + elif args['type'] == 'decklink': + input = DecklinkInput(**args, collection=self) else: raise brave.exceptions.InvalidConfiguration(f"Invalid input type '{str(args['type'])}'") diff --git a/brave/inputs/decklink.py b/brave/inputs/decklink.py new file mode 100644 index 0000000..cab3eb8 --- /dev/null +++ b/brave/inputs/decklink.py @@ -0,0 +1,108 @@ +from brave.inputs.input import Input +from gi.repository import Gst +import brave.config as config + + +class DecklinkInput(Input): + ''' + Handles input via URI. + This can be anything Playbin accepts, including local files and remote streams. + ''' + def permitted_props(self): + return { + **super().permitted_props(), + 'device': { + 'type': 'int', + 'default': 0, + }, + 'connection': { + 'type': 'int', + 'default': 1, + }, + 'mode': { + 'type': 'int', + 'default': 17, + }, + 'width': { + 'type': 'int', + 'default': 1280 + }, + 'height': { + 'type': 'int', + 'default': 720 + }, + 'xpos': { + 'type': 'int', + 'default': 0 + }, + 'ypos': { + 'type': 'int', + 'default': 0 + }, + 'zorder': { + 'type': 'int', + 'default': 1 + } + } + + def create_elements(self): + if not self.create_pipeline_from_string('decklinkvideosrc' + ' device-number=' + str(self.props['device']) + + ' connection=' + str(self.props['connection']) + + ' mode=' + str(self.props['mode']) + + ' ! videoconvert' + + self.default_video_pipeline_string_end() + + ' decklinkaudiosrc device-number=0 connection=1 ! audioconvert' + + self.default_audio_pipeline_string_end()): + return False + + self.intervideosink = self.pipeline.get_by_name('intervideosink') + self.final_video_tee = self.pipeline.get_by_name('final_video_tee') + self.final_audio_tee = self.pipeline.get_by_name('final_audio_tee') + self.handle_updated_props() + + def update(self, updates): + super().update(updates) + + # Special case: allow seeking + if self.has_video() and 'position' in updates: + try: + new_position = float(updates['position']) + if self.pipeline.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH, new_position): + self.logger.debug('Successfully updated position to %s' % new_position) + else: + self.logger.warning('Unable to est position to %s' % new_position) + except ValueError: + self.logger.warning('Invalid position %s provided' % updates['position']) + + def get_input_cap_props(self): + ''' + Parses the caps that arrive from the input, and returns them. + This allows the height/width/framerate/audio_rate to be retrieved. + ''' + elements = {} + if hasattr(self, 'intervideosink'): + elements['video'] = self.intervideosink + + props = {} + for (audioOrVideo, element) in elements.items(): + if not element: + return + caps = element.get_static_pad('sink').get_current_caps() + if not caps: + return + size = caps.get_size() + if size == 0: + return + + structure = caps.get_structure(0) + props[audioOrVideo + '_caps_string'] = structure.to_string() + if structure.has_field('framerate'): + framerate = structure.get_fraction('framerate') + props['framerate'] = framerate.value_numerator / framerate.value_denominator + if structure.has_field('height'): + props['height'] = structure.get_int('height').value + if structure.has_field('width'): + props['width'] = structure.get_int('width').value + + return props diff --git a/public/js/inputs.js b/public/js/inputs.js index 008f030..6e20c9a 100644 --- a/public/js/inputs.js +++ b/public/js/inputs.js @@ -64,6 +64,9 @@ inputsHandler._inputCardBody = (input) => { if (input.props.hasOwnProperty('freq')) details.push('
Frequency: ' + input.props.freq + 'Hz
') if (input.props.hasOwnProperty('pattern')) details.push('
Pattern: ' + inputsHandler.patternTypes[input.props.pattern] + '
') if (input.props.hasOwnProperty('wave')) details.push('
Wave: ' + inputsHandler.waveTypes[input.props.wave] + '
') + if (input.props.hasOwnProperty('device')) details.push('
Device Num: ' + input.props.device + '
') + if (input.props.hasOwnProperty('connection')) details.push('
Connection Type: ' + inputsHandler.decklinkConnection[input.props.connection] + '
') + if (input.props.hasOwnProperty('mode')) details.push('
Input Mode: ' + inputsHandler.decklinkModes[input.props.mode] + '
') if (input.hasOwnProperty('duration')) { var duration = prettyDuration(input.duration) @@ -186,12 +189,41 @@ inputsHandler._populateForm = function(input) { max: 20000 }) + var device = formGroup({ + id: 'input-device', + label: 'Device Num', + name: 'device', + type: 'number', + value: input.props.device || 0 + }) + + var connection = formGroup({ + id: 'connection-device', + label: 'Connection Type', + name: 'connection', + type: 'number', + options: inputsHandler.decklinkConnection, + initialOption: 'Select connection type', + value: input.props.connection || inputsHandler.decklinkConnection[1] + }) + + var mode = formGroup({ + id: 'mode-device', + label: 'Input Mode', + name: 'mode', + type: 'number', + options: inputsHandler.decklinkModes, + initialOption: 'Select input mode', + value: input.props.mode || inputsHandler.decklinkModes[17] + }) + var isNew = !input.hasOwnProperty('id') if (isNew) { var options = { 'uri': 'URI (for files, RTMP, RTSP and HLS)', 'image': 'Image', 'html': 'HTML (for showing a web page)', + 'decklink': 'Decklink Device', 'test_video': 'Test video stream', 'test_audio': 'Test audio stream', } @@ -241,7 +273,14 @@ inputsHandler._populateForm = function(input) { form.append(sizeBox); form.append(zOrderBox); } - + else if (input.type === 'decklink') { + if (isNew) form.append(device); + if (isNew) form.append(mode); + if (isNew) form.append(connection); + form.append(positionBox); + form.append(sizeBox); + form.append(zOrderBox); + } form.find('select[name="type"]').change(inputsHandler._handleNewFormType); } @@ -397,6 +436,50 @@ inputsHandler.waveTypes = [ 'Violet noise' ] +inputsHandler.decklinkModes = [ + 'Automatic detection (Hardware Dependant)', + 'NTSC SD 60i', + 'NTSC SD 60i (24 fps)', + 'PAL SD 50i', + 'NTSC SD 60p', + 'PAL SD 50p', + 'HD1080 23.98p', + 'HD1080 24p', + 'HD1080 25p', + 'HD1080 29.97p', + 'HD1080 30p', + 'HD1080 50i', + 'HD1080 59.94i', + 'HD1080 60i', + 'HD1080 50p', + 'HD1080 59.94p', + 'HD1080 60p', + 'HD720 50p', + 'HD720 59.94p', + 'HD720 60p', + '2k 23.98p', + '2k 24p', + '2k 25p', + '4k 23.98p', + '4k 24p', + '4k 25p', + '4k 29.97p', + '4k 30p', + '4k 50p', + '4k 59.94p', + '4k 60p', +] + +inputsHandler.decklinkConnection = [ + 'Auto (Hardware Dependant)', + 'SDI', + 'HDMI', + 'Optical SDI', + 'Component', + 'Composite', + 'S-Video', +] + function prettyDuration(d) { if (d < 0) return null var seconds = Math.floor(d / 1000000000) From 25c8f88ac91b9e0e82579cc6321acc7b272fec8c Mon Sep 17 00:00:00 2001 From: Luke Moscrop Date: Fri, 14 Dec 2018 10:14:18 +0000 Subject: [PATCH 2/3] fixed some olld comments and removed seeking as its a live source --- brave/inputs/decklink.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/brave/inputs/decklink.py b/brave/inputs/decklink.py index cab3eb8..4ef7794 100644 --- a/brave/inputs/decklink.py +++ b/brave/inputs/decklink.py @@ -5,8 +5,8 @@ class DecklinkInput(Input): ''' - Handles input via URI. - This can be anything Playbin accepts, including local files and remote streams. + Handles input via a deckoink card/device. + This can allow SDI/HDMI singals to be localy mixed with brave ''' def permitted_props(self): return { @@ -61,20 +61,6 @@ def create_elements(self): self.final_audio_tee = self.pipeline.get_by_name('final_audio_tee') self.handle_updated_props() - def update(self, updates): - super().update(updates) - - # Special case: allow seeking - if self.has_video() and 'position' in updates: - try: - new_position = float(updates['position']) - if self.pipeline.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH, new_position): - self.logger.debug('Successfully updated position to %s' % new_position) - else: - self.logger.warning('Unable to est position to %s' % new_position) - except ValueError: - self.logger.warning('Invalid position %s provided' % updates['position']) - def get_input_cap_props(self): ''' Parses the caps that arrive from the input, and returns them. From b52d010402af3792e434fe585d6a451f193439fd Mon Sep 17 00:00:00 2001 From: Luke Moscrop Date: Fri, 14 Dec 2018 10:20:48 +0000 Subject: [PATCH 3/3] Have audio device use the same as video --- brave/inputs/decklink.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/brave/inputs/decklink.py b/brave/inputs/decklink.py index 4ef7794..de3e93c 100644 --- a/brave/inputs/decklink.py +++ b/brave/inputs/decklink.py @@ -46,13 +46,14 @@ def permitted_props(self): } def create_elements(self): + #TODO: Audio is currently lcoked to HDI/HDMI mode may need to figure a btter way to auto select the best one if not self.create_pipeline_from_string('decklinkvideosrc' ' device-number=' + str(self.props['device']) + ' connection=' + str(self.props['connection']) + ' mode=' + str(self.props['mode']) + ' ! videoconvert' + self.default_video_pipeline_string_end() + - ' decklinkaudiosrc device-number=0 connection=1 ! audioconvert' + ' decklinkaudiosrc device-number=' + str(self.props['device']) + ' connection=1 ! audioconvert' + self.default_audio_pipeline_string_end()): return False